├── .gitignore ├── LICENSE ├── README.md ├── cpanel ├── authenticated.go ├── branding.go ├── chrome.go ├── cpanel.go ├── domains.go ├── feature.go ├── fileman.go ├── live.go ├── live_test.go ├── locale.go ├── nvdata.go ├── quota.go ├── ssl.go ├── ssl_test.go ├── theme.go └── zoneedit.go ├── cpanelgo.go ├── cpanelgo_test.go ├── example └── cpanelcli.go ├── go.mod └── whm ├── account.go ├── impersonation.go ├── servicessl.go ├── ssl.go ├── tweak.go ├── user.go └── whm.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Let's Encrypt for cPanel 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cPanel api in Go 2 | 3 | [![GoDoc](https://godoc.org/github.com/letsencrypt-cpanel/cpanelgo?status.svg)](https://godoc.org/github.com/letsencrypt-cpanel/cpanelgo) 4 | 5 | ## About 6 | 7 | This is a simple cPanel api written in go. 8 | 9 | Currently four intefaces are implemented, 10 | 11 | - CGI cPanel LiveApi - Designed for use in plugins, this interface will work through the preauthenticated CGI LiveApi environment as documented [here](https://documentation.cpanel.net/display/SDK/Guide+to+the+LiveAPI+System) (UAPI/API2/API1) 12 | - Authenticated JSON cPanel API (UAPI/API2/API1) 13 | - WHM (WHMAPI1) 14 | - WHM Impersonation to call cPanel API (UAPI/API2/API1) 15 | 16 | ## Example 17 | 18 | A simple command line example is provided in the example folder. 19 | 20 | ## About 21 | 22 | This API forms part of the [Let's Encrypt for cPanel](https://letsencrypt-for-cpanel.com/) plugin which allows cPanel/WHM hosters to provide free [Let's Encrypt](https://letsencrypt.org/) certificates for their clients. -------------------------------------------------------------------------------- /cpanel/authenticated.go: -------------------------------------------------------------------------------- 1 | package cpanel 2 | 3 | import ( 4 | "crypto/tls" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "log" 11 | "net/http" 12 | "os" 13 | 14 | "github.com/letsencrypt-cpanel/cpanelgo" 15 | ) 16 | 17 | type JsonApiGateway struct { 18 | Hostname string 19 | Username string 20 | Password string 21 | Insecure bool 22 | cl *http.Client 23 | } 24 | 25 | func NewJsonApi(hostname, username, password string, insecure bool) (CpanelApi, error) { 26 | c := &JsonApiGateway{ 27 | Hostname: hostname, 28 | Username: username, 29 | Password: password, 30 | Insecure: insecure, 31 | } 32 | 33 | // todo: a way to check the username/password here 34 | return CpanelApi{cpanelgo.NewApi(c)}, nil 35 | } 36 | 37 | func NewJsonApiWithClient(hostname, username, password string, insecure bool, cl *http.Client) (CpanelApi, error) { 38 | c := &JsonApiGateway{ 39 | Hostname: hostname, 40 | Username: username, 41 | Password: password, 42 | Insecure: insecure, 43 | cl: cl, 44 | } 45 | 46 | // todo: a way to check the username/password here 47 | return CpanelApi{cpanelgo.NewApi(c)}, nil 48 | } 49 | 50 | func (c *JsonApiGateway) UAPI(module, function string, arguments cpanelgo.Args, out interface{}) error { 51 | req := CpanelApiRequest{ 52 | ApiVersion: "uapi", 53 | Module: module, 54 | Function: function, 55 | Arguments: arguments, 56 | } 57 | 58 | return c.api(req, out) 59 | } 60 | 61 | func (c *JsonApiGateway) API2(module, function string, arguments cpanelgo.Args, out interface{}) error { 62 | req := CpanelApiRequest{ 63 | ApiVersion: "2", 64 | Module: module, 65 | Function: function, 66 | Arguments: arguments, 67 | } 68 | 69 | var result cpanelgo.API2Result 70 | err := c.api(req, &result) 71 | if err == nil { 72 | err = result.Error() 73 | } 74 | if err != nil { 75 | return err 76 | } 77 | 78 | return json.Unmarshal(result.Result, out) 79 | } 80 | 81 | func (c *JsonApiGateway) API1(module, function string, arguments []string, out interface{}) error { 82 | args := cpanelgo.Args{} 83 | for _, v := range arguments { 84 | args[v] = true 85 | } 86 | 87 | req := CpanelApiRequest{ 88 | ApiVersion: "1", 89 | Module: module, 90 | Function: function, 91 | Arguments: args, 92 | } 93 | 94 | return c.api(req, out) 95 | } 96 | 97 | func (c *JsonApiGateway) Close() error { 98 | return nil 99 | } 100 | 101 | func (c *JsonApiGateway) api(req CpanelApiRequest, out interface{}) error { 102 | vals := req.Arguments.Values(req.ApiVersion) 103 | reqUrl := fmt.Sprintf("https://%s:2083/", c.Hostname) 104 | switch req.ApiVersion { 105 | case "uapi": 106 | // https://hostname.example.com:2083/cpsess##########/execute/Module/function?parameter=value¶meter=value¶meter=value 107 | reqUrl += fmt.Sprintf("execute/%s/%s?%s", req.Module, req.Function, vals.Encode()) 108 | case "2": 109 | fallthrough 110 | case "1": 111 | // https://hostname.example.com:2083/cpsess##########/json-api/cpanel?cpanel_jsonapi_user=user&cpanel_jsonapi_apiversion=2&cpanel_jsonapi_module=Module&cpanel_jsonapi_func=function¶meter="value" 112 | vals.Add("cpanel_jsonapi_user", c.Username) 113 | vals.Add("cpanel_jsonapi_apiversion", req.ApiVersion) 114 | vals.Add("cpanel_jsonapi_module", req.Module) 115 | vals.Add("cpanel_jsonapi_func", req.Function) 116 | reqUrl += fmt.Sprintf("json-api/cpanel?%s", vals.Encode()) 117 | default: 118 | return fmt.Errorf("Unknown api version: %s", req.ApiVersion) 119 | } 120 | 121 | httpReq, err := http.NewRequest("GET", reqUrl, nil) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | httpReq.SetBasicAuth(c.Username, c.Password) 127 | 128 | if c.cl == nil { 129 | c.cl = &http.Client{} 130 | c.cl.Transport = &http.Transport{ 131 | DisableKeepAlives: true, 132 | MaxIdleConns: 1, 133 | MaxIdleConnsPerHost: 1, 134 | TLSClientConfig: &tls.Config{ 135 | InsecureSkipVerify: c.Insecure, 136 | }, 137 | } 138 | } 139 | 140 | resp, err := c.cl.Do(httpReq) 141 | if err != nil { 142 | return err 143 | } 144 | defer resp.Body.Close() 145 | 146 | if resp.StatusCode >= 300 { 147 | return errors.New(resp.Status) 148 | } 149 | 150 | // limit maximum response size 151 | lReader := io.LimitReader(resp.Body, int64(cpanelgo.ResponseSizeLimit)) 152 | 153 | bytes, err := ioutil.ReadAll(lReader) 154 | if err != nil { 155 | return err 156 | } 157 | 158 | if os.Getenv("DEBUG_CPANEL_RESPONSES") == "1" { 159 | log.Println(reqUrl) 160 | log.Println(resp.Status) 161 | log.Println(req.Function) 162 | log.Println(req.Arguments) 163 | log.Println(vals) 164 | log.Println(string(bytes)) 165 | } 166 | 167 | if len(bytes) == cpanelgo.ResponseSizeLimit { 168 | return errors.New("API response maximum size exceeded") 169 | } 170 | 171 | return json.Unmarshal(bytes, out) 172 | } 173 | -------------------------------------------------------------------------------- /cpanel/branding.go: -------------------------------------------------------------------------------- 1 | package cpanel 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/letsencrypt-cpanel/cpanelgo" 7 | ) 8 | 9 | // This is fucking undocumented 10 | func (c CpanelApi) BrandingInclude(name string) (cpanelgo.BaseAPI1Response, error) { 11 | var out cpanelgo.BaseAPI1Response 12 | err := c.Gateway.API1("Branding", "include", []string{name}, &out) 13 | if err == nil { 14 | err = out.Error() 15 | } 16 | return out, err 17 | } 18 | 19 | func (c CpanelApi) SetVar(key, value string) (cpanelgo.BaseAPI1Response, error) { 20 | var out cpanelgo.BaseAPI1Response 21 | err := c.Gateway.API1("setvar", "", []string{fmt.Sprintf("%s=%s", key, value)}, &out) 22 | if err == nil { 23 | err = out.Error() 24 | } 25 | return out, err 26 | } 27 | -------------------------------------------------------------------------------- /cpanel/chrome.go: -------------------------------------------------------------------------------- 1 | package cpanel 2 | 3 | import ( 4 | "github.com/letsencrypt-cpanel/cpanelgo" 5 | ) 6 | 7 | type GetDomAPIResponse struct { 8 | cpanelgo.BaseUAPIResponse 9 | Data struct { 10 | Header string `json:"header"` 11 | Footer string `json:"footer"` 12 | } `json:"data"` 13 | } 14 | 15 | func (c CpanelApi) GetDom(pageTitle string) (GetDomAPIResponse, error) { 16 | var out GetDomAPIResponse 17 | err := c.Gateway.UAPI("Chrome", "get_dom", cpanelgo.Args{ 18 | "page_title": pageTitle, 19 | }, &out) 20 | if err == nil { 21 | err = out.Error() 22 | } 23 | return out, err 24 | } 25 | -------------------------------------------------------------------------------- /cpanel/cpanel.go: -------------------------------------------------------------------------------- 1 | package cpanel 2 | 3 | import "github.com/letsencrypt-cpanel/cpanelgo" 4 | 5 | type CpanelApi struct { 6 | cpanelgo.Api 7 | } 8 | 9 | type CpanelApiRequest struct { 10 | Module string `json:"module"` 11 | RequestType string `json:"reqtype"` 12 | Function string `json:"func"` 13 | ApiVersion string `json:"apiversion"` 14 | Arguments cpanelgo.Args `json:"args"` 15 | } 16 | -------------------------------------------------------------------------------- /cpanel/domains.go: -------------------------------------------------------------------------------- 1 | package cpanel 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/letsencrypt-cpanel/cpanelgo" 7 | ) 8 | 9 | const ( 10 | ParkedStatusNotRedirected = "not redirected" 11 | ) 12 | 13 | type DomainsDataDomain struct { 14 | Domain string `json:"domain"` 15 | Ip string `json:"ip"` 16 | DocumentRoot string `json:"documentroot"` 17 | User string `json:"user"` 18 | ServerAlias string `json:"serveralias"` 19 | ServerName string `json:"servername"` 20 | } 21 | 22 | type DomainsDataApiResponse struct { 23 | cpanelgo.BaseUAPIResponse 24 | Data struct { 25 | MainDomain DomainsDataDomain `json:"main_domain"` 26 | AddonDomains []DomainsDataDomain `json:"addon_domains"` 27 | ParkedDomains []string `json:"parked_domains"` 28 | Sub_Domains []json.RawMessage `json:"sub_domains"` 29 | Subdomains []DomainsDataDomain `json:"-"` 30 | } `json:"data"` 31 | } 32 | 33 | func (dd DomainsDataApiResponse) DataList() []DomainsDataDomain { 34 | doms := append(dd.Data.AddonDomains, dd.Data.MainDomain) 35 | doms = append(doms, dd.Data.Subdomains...) 36 | return doms 37 | } 38 | 39 | func (r DomainsDataApiResponse) DomainList() []string { 40 | out := []string{} 41 | out = append(out, r.Data.MainDomain.Domain) 42 | out = append(out, r.Data.ParkedDomains...) 43 | for _, v := range r.Data.AddonDomains { 44 | out = append(out, v.Domain) 45 | } 46 | for _, v := range r.Data.Subdomains { 47 | out = append(out, v.Domain) 48 | } 49 | return out 50 | } 51 | 52 | func (c CpanelApi) DomainsData() (DomainsDataApiResponse, error) { 53 | var out DomainsDataApiResponse 54 | 55 | err := c.Gateway.UAPI("DomainInfo", "domains_data", cpanelgo.Args{ 56 | "format": "hash", 57 | }, &out) 58 | if err == nil { 59 | err = out.Error() 60 | } 61 | if err == nil { 62 | out.Data.Subdomains = []DomainsDataDomain{} 63 | for _, v := range out.Data.Sub_Domains { 64 | dec := DomainsDataDomain{} 65 | if err := json.Unmarshal(v, &dec); err == nil { 66 | out.Data.Subdomains = append(out.Data.Subdomains, dec) 67 | } 68 | } 69 | 70 | } 71 | return out, err 72 | } 73 | 74 | type SingleDomainDataApiResponse struct { 75 | Status int `json:"status"` 76 | Data struct { 77 | Domain string `json:"domain"` 78 | DocumentRoot string `json:"documentroot"` 79 | } `json:"data"` 80 | } 81 | 82 | func (c CpanelApi) SingleDomainData(domain string) (SingleDomainDataApiResponse, error) { 83 | var out SingleDomainDataApiResponse 84 | 85 | err := c.Gateway.UAPI("DomainInfo", "single_domain_data", cpanelgo.Args{ 86 | "domain": domain, 87 | }, &out) 88 | 89 | return out, err 90 | } 91 | 92 | type ParkedDomain struct { 93 | Domain string `json:"domain"` 94 | Status string `json:"status"` 95 | Dir string `json:"dir"` 96 | } 97 | 98 | type ListParkedDomainsApiResponse struct { 99 | cpanelgo.BaseAPI2Response 100 | Data []ParkedDomain `json:"data"` 101 | } 102 | 103 | func (c CpanelApi) ListParkedDomains() (ListParkedDomainsApiResponse, error) { 104 | var out ListParkedDomainsApiResponse 105 | 106 | err := c.Gateway.API2("Park", "listparkeddomains", cpanelgo.Args{}, &out) 107 | 108 | if err == nil { 109 | err = out.Error() 110 | } 111 | 112 | return out, err 113 | } 114 | 115 | type WebVhostsListDomainsApiResponse struct { 116 | cpanelgo.BaseUAPIResponse 117 | Data []VhostEntry `json:"data"` 118 | } 119 | 120 | type VhostEntry struct { 121 | Domain string `json:"domain"` 122 | VhostName string `json:"vhost_name"` 123 | VhostIsSsl int `json:"vhost_is_ssl"` 124 | ProxySubdomains []string `json:"proxy_subdomains"` 125 | } 126 | 127 | // put them into map for easy access 128 | func (vhapi WebVhostsListDomainsApiResponse) GetProxySubdomainsMap() map[string][]string { 129 | proxyDomainsMap := map[string][]string{} 130 | for _, vhd := range vhapi.Data { 131 | if len(vhd.ProxySubdomains) > 0 { 132 | proxyDomainsMap[vhd.Domain] = vhd.ProxySubdomains 133 | } 134 | } 135 | return proxyDomainsMap 136 | } 137 | 138 | func (r WebVhostsListDomainsApiResponse) GetAllProxySubdomains() []string { 139 | m := map[string]struct{}{} 140 | for _, d := range r.Data { 141 | for _, proxy := range d.ProxySubdomains { 142 | m[proxy] = struct{}{} 143 | } 144 | } 145 | res := []string{} 146 | for p := range m { 147 | res = append(res, p) 148 | } 149 | return res 150 | } 151 | 152 | func (c CpanelApi) WebVhostsListDomains() (WebVhostsListDomainsApiResponse, error) { 153 | var out WebVhostsListDomainsApiResponse 154 | 155 | err := c.Gateway.UAPI("WebVhosts", "list_domains", cpanelgo.Args{}, &out) 156 | 157 | if err == nil { 158 | err = out.Error() 159 | } 160 | 161 | return out, err 162 | } 163 | -------------------------------------------------------------------------------- /cpanel/feature.go: -------------------------------------------------------------------------------- 1 | package cpanel 2 | 3 | import "github.com/letsencrypt-cpanel/cpanelgo" 4 | 5 | func (c CpanelApi) HasFeature(name string) (string, error) { 6 | var out cpanelgo.BaseUAPIResponse 7 | err := c.Gateway.UAPI("Features", "has_feature", cpanelgo.Args{ 8 | "name": name, 9 | }, &out) 10 | if err == nil { 11 | err = out.Error() 12 | } 13 | // discard the error if its the 'unknown error' as its irrelevant to the result 14 | if err != nil && err.Error() == cpanelgo.ErrorUnknown { 15 | err = nil 16 | } 17 | return out.Message(), err 18 | } 19 | -------------------------------------------------------------------------------- /cpanel/fileman.go: -------------------------------------------------------------------------------- 1 | package cpanel 2 | 3 | import "github.com/letsencrypt-cpanel/cpanelgo" 4 | 5 | type MkdirApiResponse struct { 6 | cpanelgo.BaseAPI2Response 7 | Data []struct { 8 | Permissions string `json:"permissions"` 9 | Name string `json:"name"` 10 | Path string `json:"path"` 11 | } `json:"data"` 12 | } 13 | 14 | func (c CpanelApi) Mkdir(name, permissions, path string) (MkdirApiResponse, error) { 15 | var out MkdirApiResponse 16 | err := c.Gateway.API2("Fileman", "mkdir", cpanelgo.Args{ 17 | "path": path, 18 | "permissions": permissions, 19 | "name": name, 20 | }, &out) 21 | 22 | return out, err 23 | } 24 | 25 | type UploadFilesApiResponse struct { 26 | cpanelgo.BaseUAPIResponse 27 | Data struct { 28 | Uploads []string `json:"uploads"` 29 | Succeeded int `json:"succeeded"` 30 | Warned int `json:"warned"` 31 | Failed int `json:"failed"` 32 | } `json:"data"` 33 | } 34 | 35 | func (c CpanelApi) UploadFiles(name, contents, dir string) error { 36 | var out UploadFilesApiResponse 37 | err := c.Gateway.UAPI("Fileman", "upload_files", cpanelgo.Args{ 38 | "dir": dir, 39 | "name": name, 40 | "contents": contents, 41 | "letsencrypt": 1, 42 | }, &out) 43 | if err == nil { 44 | err = out.Error() 45 | } 46 | return err 47 | } 48 | -------------------------------------------------------------------------------- /cpanel/live.go: -------------------------------------------------------------------------------- 1 | package cpanel 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net" 11 | "strings" 12 | 13 | "log" 14 | "os" 15 | 16 | "github.com/letsencrypt-cpanel/cpanelgo" 17 | ) 18 | 19 | type LiveApiGateway struct { 20 | net.Conn 21 | } 22 | 23 | func NewLiveApi(network, address string) (CpanelApi, error) { 24 | c := &LiveApiGateway{} 25 | 26 | conn, err := net.Dial(network, address) 27 | if err != nil { 28 | return CpanelApi{}, err 29 | } 30 | c.Conn = conn 31 | 32 | if err := c.exec(``, nil); err != nil { 33 | return CpanelApi{}, fmt.Errorf("Enabling JSON: %v", err) 34 | } 35 | 36 | return CpanelApi{cpanelgo.NewApi(c)}, nil 37 | } 38 | 39 | func (c *LiveApiGateway) UAPI(module, function string, arguments cpanelgo.Args, out interface{}) error { 40 | req := CpanelApiRequest{ 41 | RequestType: "exec", 42 | ApiVersion: "uapi", 43 | Module: module, 44 | Function: function, 45 | Arguments: arguments, 46 | } 47 | 48 | return c.api(req, out) 49 | } 50 | 51 | func (c *LiveApiGateway) API2(module, function string, arguments cpanelgo.Args, out interface{}) error { 52 | req := CpanelApiRequest{ 53 | RequestType: "exec", 54 | ApiVersion: "2", 55 | Module: module, 56 | Function: function, 57 | Arguments: arguments, 58 | } 59 | 60 | return c.api(req, out) 61 | } 62 | 63 | func (c *LiveApiGateway) API1(module, function string, arguments []string, out interface{}) error { 64 | req := map[string]interface{}{ 65 | "module": module, 66 | "reqtype": "exec", 67 | "func": function, 68 | "apiversion": "1", 69 | "args": arguments, 70 | } 71 | bytes, err := json.Marshal(req) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | return c.exec(""+string(bytes)+"", out) 77 | } 78 | 79 | func (c *LiveApiGateway) Close() error { 80 | return c.Conn.Close() 81 | } 82 | 83 | func (c *LiveApiGateway) api(req CpanelApiRequest, out interface{}) error { 84 | buf, err := json.Marshal(req) 85 | if err != nil { 86 | return err 87 | } 88 | if os.Getenv("DEBUG_CPANEL_RESPONSES") == "1" { 89 | log.Println("[Lets Encrypt for cPanel] Request: ", string(buf)) 90 | } 91 | switch req.ApiVersion { 92 | case "uapi": 93 | var result cpanelgo.UAPIResult 94 | err := c.exec(""+string(buf)+"", &result) 95 | if err == nil { 96 | err = result.Error() 97 | } 98 | if err != nil { 99 | return err 100 | } 101 | 102 | if os.Getenv("DEBUG_CPANEL_RESPONSES") == "1" { 103 | log.Println("[Lets Encrypt for cPanel] UResult: ", string(result.Result)) 104 | } 105 | return json.Unmarshal(result.Result, out) 106 | case "2": 107 | var result cpanelgo.API2Result 108 | err := c.exec(""+string(buf)+"", &result) 109 | if err == nil { 110 | err = result.Error() 111 | } 112 | if err != nil { 113 | return err 114 | } 115 | if os.Getenv("DEBUG_CPANEL_RESPONSES") == "1" { 116 | log.Println("[Lets Encrypt for cPanel] 2Result: ", string(result.Result)) 117 | } 118 | return json.Unmarshal(result.Result, out) 119 | default: 120 | return c.exec(""+string(buf)+"", out) 121 | } 122 | } 123 | 124 | func endsWith(where []byte, what string) bool { 125 | n := 0 126 | i := len(where) - len(what) 127 | if i < 0 { 128 | return false 129 | } 130 | for ; i >= 0 && i < len(where); i++ { 131 | if where[i] != what[n] { 132 | return false 133 | } 134 | n++ 135 | } 136 | return true 137 | } 138 | 139 | func extractJSONString(s string) (string, error) { 140 | needles := []string{ 141 | "{", 142 | "{", 143 | ">{", 144 | } 145 | var found bool 146 | for _, needle := range needles { 147 | pos := strings.Index(s, needle) 148 | if pos == -1 { 149 | continue 150 | } 151 | s = s[pos+len(needle)-1:] 152 | found = true 153 | break 154 | } 155 | if !found { 156 | return "", fmt.Errorf("Could not find start of JSON in: %s", s) 157 | } 158 | eof := strings.Index(s, "") 159 | if eof == -1 { 160 | return "", fmt.Errorf("Does not appear to be well-formed: %s", s) 161 | } 162 | return s[:eof], nil 163 | } 164 | 165 | func (c *LiveApiGateway) exec(req string, out interface{}) error { 166 | if _, err := fmt.Fprintf(c, "%d\n%s", len(req), req); err != nil { 167 | return err 168 | } 169 | 170 | var read bytes.Buffer 171 | rd := bufio.NewReader(c) 172 | 173 | line, _, err := rd.ReadLine() // ignore isprefix 174 | for err == nil { 175 | read.Write(line) 176 | 177 | if endsWith(read.Bytes(), "") { 178 | break 179 | } 180 | 181 | // limit memory footprint of any api response 182 | if read.Len() >= cpanelgo.ResponseSizeLimit { 183 | return errors.New("Exceeded maximum API response size") 184 | } 185 | line, _, err = rd.ReadLine() 186 | } 187 | if err != nil && err != io.EOF { 188 | return err 189 | } 190 | 191 | if out == nil { 192 | return nil 193 | } 194 | 195 | asJSON, err := extractJSONString(read.String()) 196 | if err != nil { 197 | return err 198 | } 199 | return json.Unmarshal([]byte(asJSON), out) 200 | } 201 | -------------------------------------------------------------------------------- /cpanel/live_test.go: -------------------------------------------------------------------------------- 1 | package cpanel 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | func TestLiveAPIUnmarshal(t *testing.T) { 9 | inputs := []string{ 10 | `A warning occurred while processing this directive.{"func":"installed_hosts"}`, 11 | `{"func":"installed_hosts"}`, 12 | `{}`, 13 | `haha{}`, 14 | } 15 | 16 | var out interface{} 17 | for _, input := range inputs { 18 | s, err := extractJSONString(input) 19 | if err != nil { 20 | t.Errorf("%s when extracting JSON from: %s", err, input) 21 | continue 22 | } 23 | if err := json.Unmarshal([]byte(s), &out); err != nil { 24 | t.Errorf("%s when unmarshaling JSON from: %s", err, s) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /cpanel/locale.go: -------------------------------------------------------------------------------- 1 | package cpanel 2 | 3 | import "github.com/letsencrypt-cpanel/cpanelgo" 4 | 5 | type LocaleAPIResponse_UAPI struct { 6 | cpanelgo.BaseUAPIResponse 7 | Data struct { 8 | Direction string `json:"direction"` 9 | Name string `json:"name"` 10 | Locale string `json:"locale"` 11 | Encoding string `json:"encoding"` 12 | } `json:"data"` 13 | } 14 | 15 | func (c CpanelApi) GetLocaleAttributes() (LocaleAPIResponse_UAPI, error) { 16 | var out LocaleAPIResponse_UAPI 17 | err := c.Gateway.UAPI("Locale", "get_attributes", cpanelgo.Args{}, &out) 18 | if err == nil { 19 | err = out.Error() 20 | } 21 | return out, err 22 | } 23 | 24 | type LocaleAPIResponse_API2 struct { 25 | cpanelgo.BaseAPI2Response 26 | Data []struct { 27 | Locale string `json:"locale"` 28 | } `json:"data"` 29 | } 30 | 31 | func (c CpanelApi) GetUserLocale() (LocaleAPIResponse_API2, error) { 32 | var out LocaleAPIResponse_API2 33 | err := c.Gateway.API2("Locale", "get_user_locale", cpanelgo.Args{}, &out) 34 | if err == nil { 35 | err = out.Error() 36 | } 37 | return out, err 38 | } 39 | -------------------------------------------------------------------------------- /cpanel/nvdata.go: -------------------------------------------------------------------------------- 1 | package cpanel 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/letsencrypt-cpanel/cpanelgo" 7 | ) 8 | 9 | type NVDataGetApiResult struct { 10 | cpanelgo.BaseUAPIResponse 11 | Data []struct { 12 | FileName string `json:"name"` 13 | FileContents string `json:"value"` 14 | } `json:"data"` 15 | } 16 | 17 | func (c CpanelApi) GetNVData(name string) (NVDataGetApiResult, error) { 18 | var out NVDataGetApiResult 19 | err := c.Gateway.UAPI("NVData", "get", cpanelgo.Args{ 20 | "names": name, 21 | }, &out) 22 | if err == nil { 23 | err = out.Error() 24 | } 25 | return out, err 26 | } 27 | 28 | type NVDataSetApiResult struct { 29 | cpanelgo.BaseUAPIResponse 30 | Data []struct { 31 | Set string `json:"set"` 32 | } `json:"data"` 33 | } 34 | 35 | func (c CpanelApi) SetNVData(name string, data interface{}) (NVDataSetApiResult, error) { 36 | var out NVDataSetApiResult 37 | 38 | buf, err := json.Marshal(data) 39 | if err != nil { 40 | return out, err 41 | } 42 | 43 | err = c.Gateway.UAPI("NVData", "set", cpanelgo.Args{ 44 | "names": name, 45 | name: string(buf), 46 | }, &out) 47 | 48 | if err == nil { 49 | err = out.Error() 50 | } 51 | 52 | return out, err 53 | } 54 | -------------------------------------------------------------------------------- /cpanel/quota.go: -------------------------------------------------------------------------------- 1 | package cpanel 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "strconv" 7 | 8 | "github.com/letsencrypt-cpanel/cpanelgo" 9 | ) 10 | 11 | type GetQuotaInfoApiResponse struct { 12 | cpanelgo.BaseUAPIResponse 13 | Data struct { 14 | UnderQuotaOverall *json.RawMessage `json:"under_quota_overall"` 15 | } `json:"data"` 16 | } 17 | 18 | func (q GetQuotaInfoApiResponse) IsUnderQuota() bool { 19 | // seems to be a string when "1" but an int when 0 20 | // cover all the bases 21 | 22 | // not present, assume under quota 23 | if q.Data.UnderQuotaOverall == nil { 24 | return true 25 | } 26 | 27 | // string, parse 28 | s := "" 29 | if err := json.Unmarshal(*q.Data.UnderQuotaOverall, &s); err == nil { 30 | n, _ := strconv.ParseInt(s, 10, 0) 31 | return n == 1 32 | } 33 | 34 | // int 35 | i := 0 36 | if err := json.Unmarshal(*q.Data.UnderQuotaOverall, &s); err == nil { 37 | return i == 1 38 | } 39 | 40 | return false 41 | } 42 | 43 | func (c CpanelApi) GetQuotaInfo() (GetQuotaInfoApiResponse, error) { 44 | var out GetQuotaInfoApiResponse 45 | err := c.Gateway.UAPI("Quota", "get_quota_info", cpanelgo.Args{}, &out) 46 | if err == nil { 47 | err = out.Error() 48 | } 49 | return out, err 50 | } 51 | -------------------------------------------------------------------------------- /cpanel/ssl.go: -------------------------------------------------------------------------------- 1 | package cpanel 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "time" 7 | 8 | "errors" 9 | "fmt" 10 | 11 | "crypto/x509" 12 | "encoding/pem" 13 | 14 | "github.com/letsencrypt-cpanel/cpanelgo" 15 | ) 16 | 17 | type CpanelSslCertificate struct { 18 | Domains []string `json:"domains"` 19 | CommonName cpanelgo.MaybeCommonNameString `json:"subject.commonName"` 20 | IsSelfSigned cpanelgo.MaybeInt64 `json:"is_self_signed"` 21 | Id string `json:"id"` 22 | NotAfter cpanelgo.MaybeInt64 `json:"not_after"` 23 | OrgName string `json:"issuer.organizationName"` 24 | DomainIsConfigured cpanelgo.MaybeInt64 `json:"domain_is_configured"` // Doesn't actually work 25 | } 26 | 27 | func (s CpanelSslCertificate) Expiry() time.Time { 28 | return time.Unix(int64(s.NotAfter), 0) 29 | } 30 | 31 | type ListSSLKeysAPIResponse struct { 32 | cpanelgo.BaseUAPIResponse 33 | Data struct { 34 | Created cpanelgo.MaybeInt64 `json:"created"` 35 | Modulus string `json:"modulus"` 36 | Id string `json:"id"` 37 | FriendlyName string `json:"friendly_name"` 38 | ModulusLength int `json:"modulus_length"` 39 | } `json:"data"` 40 | } 41 | 42 | func (c CpanelApi) ListSSLKeys() (ListSSLKeysAPIResponse, error) { 43 | var out ListSSLKeysAPIResponse 44 | err := c.Gateway.UAPI("SSL", "list_keys", nil, &out) 45 | if err == nil { 46 | err = out.Error() 47 | } 48 | return out, err 49 | } 50 | 51 | type ListSSLCertsAPIResponse struct { 52 | cpanelgo.BaseUAPIResponse 53 | Data []CpanelSslCertificate `json:"data"` 54 | } 55 | 56 | func (c CpanelApi) ListSSLCerts() (ListSSLCertsAPIResponse, error) { 57 | var out ListSSLCertsAPIResponse 58 | err := c.Gateway.UAPI("SSL", "list_certs", nil, &out) 59 | if err == nil { 60 | err = out.Error() 61 | } 62 | return out, err 63 | } 64 | 65 | type InstalledCertificate struct { 66 | Certificate CpanelSslCertificate `json:"certificate"` 67 | CertificateText string `json:"certificate_text"` 68 | FQDNs []string `json:"fqdns"` 69 | } 70 | 71 | type InstalledHostsApiResponse struct { 72 | cpanelgo.BaseUAPIResponse 73 | Data []InstalledCertificate `json:"data"` 74 | } 75 | 76 | func (r InstalledHostsApiResponse) HasDomain(d string) bool { 77 | for _, h := range r.Data { 78 | if strings.ToLower(d) == strings.ToLower(string(h.Certificate.CommonName)) { 79 | return true 80 | } 81 | for _, v := range h.Certificate.Domains { 82 | if strings.ToLower(d) == strings.ToLower(v) { 83 | return true 84 | } 85 | } 86 | } 87 | return false 88 | } 89 | 90 | func (r InstalledHostsApiResponse) GetCertificateForDomain(d string) (CpanelSslCertificate, bool) { 91 | for _, h := range r.Data { 92 | if strings.ToLower(d) == strings.ToLower(string(h.Certificate.CommonName)) { 93 | return h.Certificate, true 94 | } 95 | for _, v := range h.Certificate.Domains { 96 | if strings.ToLower(d) == strings.ToLower(v) { 97 | return h.Certificate, true 98 | } 99 | } 100 | } 101 | return CpanelSslCertificate{}, false 102 | } 103 | 104 | func (r InstalledHostsApiResponse) HasValidDomain(wanted string, expiryCutoff time.Time) bool { 105 | wanted = strings.ToLower(wanted) 106 | splitWanted := strings.Split(wanted, ".") 107 | 108 | // first determine if there is an exact match for the domain available 109 | hasExactDomain := false 110 | for _, h := range r.Data { 111 | if wanted == strings.ToLower(string(h.Certificate.CommonName)) { 112 | hasExactDomain = true 113 | } 114 | for _, v := range h.Certificate.Domains { 115 | if wanted == strings.ToLower(v) { 116 | hasExactDomain = true 117 | } 118 | } 119 | } 120 | 121 | isDomainCoveredByName := func(name string) bool { 122 | name = strings.ToLower(name) 123 | 124 | if wanted == name { 125 | return true 126 | } 127 | 128 | // Only other way this name can cover the wanted name is if the name is a wildcard 129 | if hasExactDomain || !strings.HasPrefix(name, "*.") { 130 | return false 131 | } 132 | 133 | // Strip off the first identifier 134 | splitName := strings.Split(name, ".") 135 | 136 | if len(splitWanted) < 2 || len(splitName) < 2 { 137 | return false 138 | } 139 | 140 | // Compare the name without the first identifier (because we know one of them is * already) 141 | return strings.Join(splitWanted[1:], ".") == strings.Join(splitName[1:], ".") 142 | } 143 | 144 | for _, h := range r.Data { 145 | // Ignore self-signed and 'expiring'/expired certificates 146 | if h.Certificate.IsSelfSigned == 1 || h.Certificate.Expiry().Before(expiryCutoff) { 147 | continue 148 | } 149 | if isDomainCoveredByName(string(h.Certificate.CommonName)) { 150 | return true 151 | } 152 | for _, v := range h.Certificate.Domains { 153 | if isDomainCoveredByName(v) { 154 | return true 155 | } 156 | } 157 | } 158 | return false 159 | } 160 | 161 | // In AutoSSL we want to avoid issuing certificates into virtual hosts that already have 162 | // a valid certificate installed, whether or not that certificate actually covers `domain` 163 | func (r InstalledHostsApiResponse) DoesAnyValidCertificateOverlapVhostsWith(domain string, expiryCutoff time.Time) bool { 164 | domain = strings.ToLower(domain) 165 | split := strings.Split(domain, ".") 166 | 167 | isDomainCoveredByName := func(name string) bool { 168 | name = strings.ToLower(name) 169 | if domain == name { 170 | return true 171 | } 172 | if !strings.HasPrefix(name, "*.") { 173 | return false 174 | } 175 | splitName := strings.Split(name, ".") 176 | if len(split) < 2 || len(splitName) < 2 { 177 | return false 178 | } 179 | return strings.Join(split[1:], ".") == strings.Join(splitName[1:], ".") 180 | } 181 | 182 | for _, h := range r.Data { 183 | // Intentionally not paying attention to the validity 184 | if h.Certificate.IsSelfSigned == 1 || h.Certificate.Expiry().Before(expiryCutoff) { 185 | continue 186 | } 187 | for _, fqdn := range h.FQDNs { 188 | if isDomainCoveredByName(fqdn) { 189 | return true 190 | } 191 | } 192 | } 193 | 194 | return false 195 | } 196 | 197 | func (c CpanelApi) InstalledHosts() (InstalledHostsApiResponse, error) { 198 | var out InstalledHostsApiResponse 199 | 200 | if err := c.Gateway.UAPI("SSL", "installed_hosts", nil, &out); err != nil { 201 | return out, err 202 | } 203 | 204 | // If there is a non-transport error/warning and we didn't find any SSL 205 | // virtual hosts, report this as a fatal error. 206 | if err := out.Error(); err != nil && len(out.Data) == 0 { 207 | return out, err 208 | } 209 | 210 | return out, nil 211 | } 212 | 213 | type GenerateSSLKeyAPIResponse struct { 214 | cpanelgo.BaseUAPIResponse 215 | Data struct { 216 | Created cpanelgo.MaybeInt64 `json:"created"` 217 | Modulus string `json:"modulus"` 218 | Text string `json:"text"` 219 | Id string `json:"id"` 220 | FriendlyName string `json:"friendly_name"` 221 | ModulusLength int `json:"modulus_length"` 222 | } `json:"data"` 223 | } 224 | 225 | func (c CpanelApi) GenerateSSLKey(keySize int, friendlyName string) (GenerateSSLKeyAPIResponse, error) { 226 | var out GenerateSSLKeyAPIResponse 227 | err := c.Gateway.UAPI("SSL", "generate_key", cpanelgo.Args{ 228 | "key_size": strconv.Itoa(keySize), 229 | "friendly_name": friendlyName, 230 | }, &out) 231 | if err == nil { 232 | err = out.Error() 233 | } 234 | return out, err 235 | } 236 | 237 | type InstallSSLKeyAPIResponse struct { 238 | cpanelgo.BaseUAPIResponse 239 | Data struct { 240 | Action string `json:"action"` 241 | CertId string `json:"cert_id"` 242 | Domain string `json:"domain"` 243 | Html string `json:"html"` 244 | Ip string `json:"ip"` 245 | KeyId string `json:"key_id"` 246 | Message string `json:"message"` 247 | StatusMsg string `json:"statusmsg"` 248 | User string `json:"user"` 249 | WarningDomains []string `json:"warning_domains"` 250 | WorkingDomains []string `json:"working_domains"` 251 | ExtraCertificateDomains []string `json:"extra_certificate_domains"` 252 | } `json:"data"` 253 | } 254 | 255 | func (c CpanelApi) InstallSSLKey(domain string, cert string, key string, cabundle string) (InstallSSLKeyAPIResponse, error) { 256 | var out InstallSSLKeyAPIResponse 257 | err := c.Gateway.UAPI("SSL", "install_ssl", cpanelgo.Args{ 258 | "domain": domain, 259 | "cert": cert, 260 | "key": key, 261 | "cabundle": cabundle, 262 | }, &out) 263 | if err == nil { 264 | err = out.Error() 265 | } 266 | 267 | // err = errors.New("FAKE unknown error") 268 | // out.Data.CertId = "" 269 | // out.Data.KeyId = "" 270 | 271 | // unknown error ocsp failing condition 272 | // certificate is installed but no certid/keyid returned 273 | // attempt to find the certid for installed status 274 | // TODO: remove this prior to pushing to github 275 | if err != nil && strings.Contains(err.Error(), "unknown error") { 276 | // if the api actually returned the cert id proper, we can just ignore the error and continue 277 | if out.Data.CertId != "" { 278 | err = nil 279 | out.Data.Message = fmt.Sprintf("The SSL certificate is now installed onto the domain “%s”", domain) 280 | goto DORETURN 281 | } 282 | // otherwise try to find the installed certid of the given cert 283 | installedCertId, findCertErr := c.findExistingCertificate(cert) 284 | if findCertErr != nil { 285 | err = fmt.Errorf("Error checking installed ssl certificate: %v", findCertErr) 286 | goto DORETURN 287 | } 288 | if installedCertId == "" { 289 | err = errors.New("Unable to find installed certificate") 290 | goto DORETURN 291 | } 292 | out.Data.CertId = installedCertId 293 | out.Data.Message = fmt.Sprintf("The SSL certificate is now installed onto the domain “%s”", domain) 294 | err = nil 295 | } 296 | 297 | DORETURN: 298 | return out, err 299 | } 300 | 301 | // TODO: remove this prior to pushing to github 302 | func decodeToCert(s string) (*x509.Certificate, error) { 303 | b, _ := pem.Decode([]byte(s)) 304 | if b == nil { 305 | return nil, errors.New("Unable to decode pem") 306 | } 307 | 308 | cert, err := x509.ParseCertificate(b.Bytes) 309 | if err != nil { 310 | return nil, err 311 | } 312 | 313 | return cert, nil 314 | } 315 | 316 | // TODO: remove this prior to pushing to github 317 | func (c CpanelApi) findExistingCertificate(certPem string) (string, error) { 318 | 319 | hosts, err := c.InstalledHosts() 320 | if err != nil { 321 | return "", err 322 | } 323 | 324 | cert, err := decodeToCert(certPem) 325 | if err != nil { 326 | return "", err 327 | } 328 | 329 | for _, h := range hosts.Data { 330 | c, err := decodeToCert(h.CertificateText) 331 | if err == nil { 332 | if cert.SerialNumber.Cmp(c.SerialNumber) == 0 { 333 | return h.Certificate.Id, nil 334 | } 335 | } 336 | } 337 | 338 | return "", errors.New("Unable to find installed certificate for domain") 339 | } 340 | 341 | func (c CpanelApi) DeleteSSL(domain string) (cpanelgo.BaseUAPIResponse, error) { 342 | var out cpanelgo.BaseUAPIResponse 343 | err := c.Gateway.UAPI("SSL", "delete_ssl", cpanelgo.Args{ 344 | "domain": domain, 345 | }, &out) 346 | if err == nil { 347 | err = out.Error() 348 | } 349 | return out, err 350 | } 351 | 352 | func (c CpanelApi) DeleteCert(certId string) (cpanelgo.BaseUAPIResponse, error) { 353 | var out cpanelgo.BaseUAPIResponse 354 | err := c.Gateway.UAPI("SSL", "delete_cert", cpanelgo.Args{ 355 | "id": certId, 356 | }, &out) 357 | if err == nil { 358 | err = out.Error() 359 | } 360 | return out, err 361 | } 362 | 363 | func (c CpanelApi) DeleteKey(keyId string) (cpanelgo.BaseUAPIResponse, error) { 364 | var out cpanelgo.BaseUAPIResponse 365 | err := c.Gateway.UAPI("SSL", "delete_key", cpanelgo.Args{ 366 | "id": keyId, 367 | }, &out) 368 | if err == nil { 369 | err = out.Error() 370 | } 371 | return out, err 372 | } 373 | 374 | type EnableMailSNIAPIResponse struct { 375 | cpanelgo.BaseUAPIResponse 376 | Data struct { 377 | UpdatedDomains map[string]int `json:"updated_domains"` 378 | FailedDomains map[string]interface{} `json:"failed_domains"` 379 | } `json:"data"` 380 | } 381 | 382 | func (c CpanelApi) EnableMailSNI(domains ...string) (EnableMailSNIAPIResponse, error) { 383 | var out EnableMailSNIAPIResponse 384 | err := c.Gateway.UAPI("SSL", "enable_mail_sni", cpanelgo.Args{ 385 | "domains": strings.Join(domains, "|"), 386 | }, &out) 387 | if err == nil { 388 | err = out.Error() 389 | } 390 | return out, err 391 | } 392 | 393 | type IsMailSNISupportedAPIResponse struct { 394 | cpanelgo.BaseUAPIResponse 395 | Data int `json:"data"` 396 | } 397 | 398 | func (c CpanelApi) IsMailSNISupported() (IsMailSNISupportedAPIResponse, error) { 399 | var out IsMailSNISupportedAPIResponse 400 | err := c.Gateway.UAPI("SSL", "is_mail_sni_supported", cpanelgo.Args{}, &out) 401 | if err == nil { 402 | err = out.Error() 403 | } 404 | return out, err 405 | } 406 | 407 | type MailSNIStatusAPIResponse struct { 408 | cpanelgo.BaseUAPIResponse 409 | Data struct { 410 | Enabled int `json:"enabled"` 411 | } `json:"data"` 412 | } 413 | 414 | func (c CpanelApi) MailSNIStatus(domain string) (MailSNIStatusAPIResponse, error) { 415 | var out MailSNIStatusAPIResponse 416 | err := c.Gateway.UAPI("SSL", "mail_sni_status", cpanelgo.Args{ 417 | "domain": domain, 418 | }, &out) 419 | if err == nil { 420 | err = out.Error() 421 | } 422 | return out, err 423 | } 424 | 425 | type RebuildMailSNIConfigAPIResponse struct { 426 | cpanelgo.BaseUAPIResponse 427 | Data struct { 428 | Success int `json:"success"` 429 | } 430 | } 431 | 432 | func (c CpanelApi) RebuildMailSNIConfig() (RebuildMailSNIConfigAPIResponse, error) { 433 | var out RebuildMailSNIConfigAPIResponse 434 | err := c.Gateway.UAPI("SSL", "rebuild_mail_sni_config", cpanelgo.Args{ 435 | "reload_dovecot": 1, 436 | }, &out) 437 | if err == nil { 438 | err = out.Error() 439 | } 440 | return out, err 441 | } 442 | -------------------------------------------------------------------------------- /cpanel/ssl_test.go: -------------------------------------------------------------------------------- 1 | package cpanel 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/letsencrypt-cpanel/cpanelgo" 8 | ) 9 | 10 | const ( 11 | d_Feb_16_2020 = 1581811200 12 | d_March_16_2020 = 1584316800 13 | d_May_17_2020 = 1589673600 14 | ) 15 | 16 | func installedCert(cn string, sans []string, notAfter int64) InstalledCertificate { 17 | return InstalledCertificate{ 18 | Certificate: CpanelSslCertificate{ 19 | IsSelfSigned: cpanelgo.MaybeInt64(0), 20 | CommonName: cpanelgo.MaybeCommonNameString(cn), 21 | Domains: sans, 22 | NotAfter: cpanelgo.MaybeInt64(notAfter), 23 | }, 24 | } 25 | } 26 | 27 | func TestWildcardDoesntClobberOtherCertificates(t *testing.T) { 28 | cutoff := time.Unix(d_Feb_16_2020, 0).Add(time.Duration(31) * 24 * time.Hour) 29 | data := []InstalledCertificate{ 30 | installedCert("home.example.com", []string{"home.example.com"}, d_March_16_2020), 31 | installedCert("node.example.com", []string{"node.example.com"}, d_March_16_2020), 32 | installedCert("example.com", []string{"example.com", "*.example.com"}, d_May_17_2020), 33 | installedCert("fake.test.com", nil, d_May_17_2020), 34 | installedCert("*.test.com", nil, d_March_16_2020), 35 | } 36 | 37 | tests := []struct { 38 | apiResp InstalledHostsApiResponse 39 | domain string 40 | cutoff time.Time 41 | result bool 42 | }{ 43 | // node.example.com is expiring 44 | { 45 | apiResp: InstalledHostsApiResponse{Data: data}, 46 | domain: "node.example.com", 47 | cutoff: cutoff, 48 | result: false, 49 | }, 50 | // node.example.com is expiring 51 | { 52 | apiResp: InstalledHostsApiResponse{Data: []InstalledCertificate{data[2], data[1]}}, 53 | domain: "node.example.com", 54 | cutoff: cutoff, 55 | result: false, 56 | }, 57 | // home.example.com is expiring 58 | { 59 | apiResp: InstalledHostsApiResponse{Data: data}, 60 | domain: "home.example.com", 61 | cutoff: cutoff, 62 | result: false, 63 | }, 64 | // fake.test.com exists and is not expiring 65 | { 66 | apiResp: InstalledHostsApiResponse{Data: data}, 67 | domain: "fake.test.com", 68 | cutoff: cutoff, 69 | result: true, 70 | }, 71 | // example.com exists and is not expiring 72 | { 73 | apiResp: InstalledHostsApiResponse{Data: data}, 74 | domain: "example.com", 75 | cutoff: cutoff, 76 | result: true, 77 | }, 78 | // node.example.com exists and is not expiring @ cutoff of d_March_16_2020 79 | { 80 | apiResp: InstalledHostsApiResponse{Data: data}, 81 | domain: "node.example.com", 82 | cutoff: time.Unix(d_March_16_2020, 0), 83 | result: true, 84 | }, 85 | // *.test.com exists and is expiring 86 | { 87 | apiResp: InstalledHostsApiResponse{Data: data}, 88 | domain: "*.test.com", 89 | cutoff: cutoff, 90 | result: false, 91 | }, 92 | // *.test.com exists and is not expiring 93 | { 94 | apiResp: InstalledHostsApiResponse{Data: data}, 95 | domain: "*.test.com", 96 | cutoff: time.Unix(d_March_16_2020, 0), 97 | result: true, 98 | }, 99 | } 100 | 101 | for _, test := range tests { 102 | valid := test.apiResp.HasValidDomain(test.domain, test.cutoff) 103 | if valid != test.result { 104 | t.Fatalf("domain %q HasValidDomain expected: %t, got: %t", test.domain, test.result, valid) 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /cpanel/theme.go: -------------------------------------------------------------------------------- 1 | package cpanel 2 | 3 | import "github.com/letsencrypt-cpanel/cpanelgo" 4 | 5 | type GetThemeAPIResponse struct { 6 | cpanelgo.BaseUAPIResponse 7 | Theme string `json:"data"` 8 | } 9 | 10 | func (c CpanelApi) GetTheme() (GetThemeAPIResponse, error) { 11 | var out GetThemeAPIResponse 12 | err := c.Gateway.UAPI("Themes", "get_theme_base", cpanelgo.Args{}, &out) 13 | if err == nil { 14 | err = out.Error() 15 | } 16 | return out, err 17 | } 18 | -------------------------------------------------------------------------------- /cpanel/zoneedit.go: -------------------------------------------------------------------------------- 1 | package cpanel 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/letsencrypt-cpanel/cpanelgo" 8 | ) 9 | 10 | type ZoneRecord struct { 11 | Name string `json:"name"` 12 | Record string `json:"record"` 13 | Type string `json:"type"` 14 | Line int `json:"line"` 15 | } 16 | 17 | type FetchZoneApiResponse struct { 18 | cpanelgo.BaseAPI2Response 19 | Data []struct { 20 | Records []ZoneRecord `json:"record"` 21 | Status int `json:"status"` 22 | StatusMessage string `json:"statusmsg"` 23 | } `json:"data"` 24 | } 25 | 26 | // Returns line number 27 | func (r FetchZoneApiResponse) Find(name, rrType string) (bool, []int) { 28 | if len(r.Data) == 0 { 29 | return false, []int{} 30 | } 31 | var lines []int 32 | name = strings.ToLower(name) 33 | for _, v := range r.Data[0].Records { 34 | if v.Type == rrType && strings.ToLower(v.Name) == name { 35 | lines = append(lines, v.Line) 36 | } 37 | } 38 | return len(lines) > 0, lines 39 | } 40 | 41 | func (c CpanelApi) FetchZone(domain, types string) (FetchZoneApiResponse, error) { 42 | var out FetchZoneApiResponse 43 | 44 | err := c.Gateway.API2("ZoneEdit", "fetchzone", cpanelgo.Args{ 45 | "domain": domain, 46 | "type": types, // can be multiple CNAME,A,AAAA 47 | }, &out) 48 | 49 | if err == nil && out.Event.Result != 1 { 50 | err = errors.New(out.Event.Reason) 51 | } 52 | 53 | if err == nil && len(out.Data) > 0 && out.Data[0].Status != 1 { 54 | err = errors.New(out.Data[0].StatusMessage) 55 | } 56 | 57 | return out, err 58 | } 59 | 60 | type AddZoneTextRecordResponse struct { 61 | cpanelgo.BaseAPI2Response 62 | Data []struct { 63 | Result struct { 64 | Status int `json:"status"` 65 | StatusMessage string `json:"statusmsg"` 66 | } `json:"result"` 67 | } `json:"data"` 68 | } 69 | 70 | func (c CpanelApi) AddZoneTextRecord(zone, name, txtData, ttl string) error { 71 | var out AddZoneTextRecordResponse 72 | 73 | err := c.Gateway.API2("ZoneEdit", "add_zone_record", cpanelgo.Args{ 74 | "domain": zone, 75 | "name": name, 76 | "type": "TXT", 77 | "txtdata": txtData, 78 | "ttl": ttl, 79 | }, &out) 80 | 81 | if err == nil && out.Event.Result != 1 { 82 | err = errors.New(out.Event.Reason) 83 | } 84 | 85 | if err == nil && len(out.Data) > 0 && out.Data[0].Result.Status != 1 { 86 | err = errors.New(out.Data[0].Result.StatusMessage) 87 | } 88 | 89 | return err 90 | } 91 | 92 | type EditZoneTextRecordResponse struct { 93 | cpanelgo.BaseAPI2Response 94 | Data []struct { 95 | Result struct { 96 | Status int `json:"status"` 97 | StatusMessage string `json:"statusmsg"` 98 | } `json:"result"` 99 | } `json:"data"` 100 | } 101 | 102 | func (c CpanelApi) EditZoneTextRecord(line int, zone, txtData, ttl string) error { 103 | var out EditZoneTextRecordResponse 104 | 105 | err := c.Gateway.API2("ZoneEdit", "edit_zone_record", cpanelgo.Args{ 106 | "domain": zone, 107 | "type": "TXT", 108 | "txtdata": txtData, 109 | "line": line, 110 | "ttl": ttl, 111 | }, &out) 112 | 113 | if err == nil && out.Event.Result != 1 { 114 | err = errors.New(out.Event.Reason) 115 | } 116 | 117 | if err == nil && len(out.Data) > 0 && out.Data[0].Result.Status != 1 { 118 | err = errors.New(out.Data[0].Result.StatusMessage) 119 | } 120 | 121 | return err 122 | } 123 | 124 | type FetchZonesApiResponse struct { 125 | cpanelgo.BaseAPI2Response 126 | Data []struct { 127 | Status int `json:"status"` 128 | StatusMessage string `json:"statusmsg"` 129 | Zones map[string][]string `json:"zones"` 130 | } `json:"data"` 131 | } 132 | 133 | func (r FetchZonesApiResponse) FindRootForName(name string) string { 134 | if len(r.Data) == 0 { 135 | return "" 136 | } 137 | zones := r.Data[0].Zones 138 | // Strip labels until we find one that actually has records 139 | for { 140 | list, exists := zones[name] 141 | if exists && len(list) > 0 { 142 | return name 143 | } 144 | 145 | idx := strings.Index(name, ".") 146 | if idx == -1 || idx == (len(name)-1) { 147 | return "" 148 | } 149 | 150 | name = name[idx+1:] 151 | } 152 | } 153 | 154 | func (c CpanelApi) FetchZones() (FetchZonesApiResponse, error) { 155 | var out FetchZonesApiResponse 156 | 157 | err := c.Gateway.API2("ZoneEdit", "fetchzones", cpanelgo.Args{}, &out) 158 | 159 | if err == nil && out.Event.Result != 1 { 160 | err = errors.New(out.Event.Reason) 161 | } 162 | 163 | if err == nil && len(out.Data) > 0 && out.Data[0].Status != 1 { 164 | err = errors.New(out.Data[0].StatusMessage) 165 | } 166 | 167 | return out, err 168 | } 169 | -------------------------------------------------------------------------------- /cpanelgo.go: -------------------------------------------------------------------------------- 1 | package cpanelgo 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/url" 8 | "os" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | const ( 14 | megabyte = 1 * 1024 * 1024 15 | ErrorUnknown = "Unknown" 16 | ) 17 | 18 | var ( 19 | ResponseSizeLimit = (20 * megabyte) + 1337 20 | ) 21 | 22 | func init() { 23 | if respSizeLimitOverride := os.Getenv("CPANELGO_RESPONSE_SIZE_LIMIT"); respSizeLimitOverride != "" { 24 | conv, err := strconv.Atoi(respSizeLimitOverride) 25 | if err != nil { 26 | return 27 | } 28 | if conv >= (5*megabyte)+1337 { 29 | ResponseSizeLimit = conv 30 | } 31 | } 32 | } 33 | 34 | type BaseResult struct { 35 | ErrorString string `json:"error"` 36 | } 37 | 38 | func (r BaseResult) Error() error { 39 | if r.ErrorString == "" { 40 | return nil 41 | } 42 | return errors.New(r.ErrorString) 43 | } 44 | 45 | type UAPIResult struct { 46 | BaseResult 47 | Result json.RawMessage `json:"result"` 48 | } 49 | 50 | type API2Result struct { 51 | BaseResult 52 | Result json.RawMessage `json:"cpanelresult"` 53 | } 54 | 55 | type BaseUAPIResponse struct { 56 | BaseResult 57 | StatusCode int `json:"status"` 58 | Errors []string `json:"errors"` 59 | Messages []string `json:"messages"` 60 | } 61 | 62 | func (r BaseUAPIResponse) Error() error { 63 | if r.StatusCode == 1 { 64 | return nil 65 | } 66 | if err := r.BaseResult.Error(); err != nil { 67 | return err 68 | } 69 | if len(r.Errors) == 0 { 70 | return errors.New(ErrorUnknown) 71 | } 72 | return errors.New(strings.Join(r.Errors, "\n")) 73 | } 74 | 75 | func (r BaseUAPIResponse) Message() string { 76 | if r.Messages == nil || len(r.Messages) == 0 { 77 | return "" 78 | } 79 | return strings.Join(r.Messages, "\n") 80 | } 81 | 82 | type BaseAPI2Response struct { 83 | BaseResult 84 | Event struct { 85 | Result int `json:"result"` 86 | Reason string `json:"reason"` 87 | } `json:"event"` 88 | } 89 | 90 | func (r BaseAPI2Response) Error() error { 91 | if r.Event.Result == 1 { 92 | return nil 93 | } 94 | err := r.BaseResult.Error() 95 | if err != nil { 96 | return err 97 | } 98 | if len(r.Event.Reason) == 0 { 99 | return errors.New("Unknown") 100 | } 101 | return errors.New(r.Event.Reason) 102 | } 103 | 104 | type BaseAPI1Response struct { 105 | // other stuff here "apiversion":"1","type":"event","module":"Serverinfo","func":"servicestatus","source":"module" 106 | Data struct { 107 | Result string `json:"result"` 108 | } `json:"data"` 109 | ErrorString string `json:"error"` 110 | // "event":{"result":1,"reason":"blah blah"}} 111 | Event struct { 112 | Result int `json:"result"` 113 | Reason string `json:"reason"` 114 | } `json:"event"` 115 | } 116 | 117 | func (r BaseAPI1Response) Error() error { 118 | if r.ErrorString != "" { 119 | return errors.New(r.ErrorString) 120 | } 121 | if r.Event.Result != 1 { 122 | // if the result != 1 the reason usually present in error ^ so kinda redundant to check, but check just in case 123 | if len(r.Event.Reason) == 0 { 124 | return errors.New("Unknown") 125 | } 126 | return errors.New(r.Event.Reason) 127 | } 128 | return nil 129 | } 130 | 131 | type Args map[string]interface{} 132 | 133 | func (a Args) Values(apiVersion string) url.Values { 134 | vals := url.Values{} 135 | for k, v := range a { 136 | if apiVersion == "1" { 137 | kv := strings.SplitN(k, "=", 2) 138 | if len(kv) == 1 { 139 | vals.Add(kv[0], "") 140 | } else if len(kv) == 2 { 141 | vals.Add(kv[0], kv[1]) 142 | } 143 | } else { 144 | vals.Add(k, fmt.Sprintf("%v", v)) 145 | } 146 | } 147 | return vals 148 | } 149 | 150 | type ApiGateway interface { 151 | UAPI(module, function string, arguments Args, out interface{}) error 152 | API2(module, function string, arguments Args, out interface{}) error 153 | API1(module, function string, arguments []string, out interface{}) error 154 | Close() error 155 | } 156 | 157 | type Api struct { 158 | Gateway ApiGateway 159 | } 160 | 161 | func NewApi(gw ApiGateway) Api { 162 | return Api{ 163 | Gateway: gw, 164 | } 165 | } 166 | 167 | func (a Api) Close() error { 168 | if a.Gateway != nil { 169 | return a.Gateway.Close() 170 | } else { 171 | return nil 172 | } 173 | } 174 | 175 | type MaybeInt64 int64 176 | 177 | func (m *MaybeInt64) MarshalJSON() ([]byte, error) { 178 | return json.Marshal(int64(*m)) 179 | } 180 | 181 | func (m *MaybeInt64) UnmarshalJSON(buf []byte) error { 182 | var out interface{} 183 | if err := json.Unmarshal(buf, &out); err != nil { 184 | return err 185 | } 186 | 187 | switch v := out.(type) { 188 | case string: 189 | if len(v) == 0 { 190 | *m = 0 191 | break 192 | } 193 | 194 | f, err := strconv.ParseFloat(v, 64) 195 | if err != nil { 196 | return err 197 | } 198 | *m = MaybeInt64(f) 199 | case float64: 200 | *m = MaybeInt64(v) 201 | case nil: 202 | *m = 0 203 | default: 204 | return errors.New("Not a string or int64") 205 | } 206 | 207 | return nil 208 | } 209 | 210 | /* 211 | "subject.commonName" : { 212 | "commonName" : "mail.l33t.website" 213 | }, 214 | or 215 | "subject.commonName" : "mail.l33t.website", 216 | */ 217 | type MaybeCommonNameString string 218 | 219 | func (m *MaybeCommonNameString) MarshalJSON() ([]byte, error) { 220 | return json.Marshal(string(*m)) 221 | } 222 | 223 | func (m *MaybeCommonNameString) UnmarshalJSON(buf []byte) error { 224 | var out interface{} 225 | if err := json.Unmarshal(buf, &out); err != nil { 226 | return err 227 | } 228 | 229 | switch v := out.(type) { 230 | case string: 231 | *m = MaybeCommonNameString(v) 232 | case map[string]interface{}: 233 | vv, ok := v["commonName"] 234 | if !ok { 235 | *m = "" 236 | return nil 237 | } 238 | vvv, ok := vv.(string) 239 | if !ok { 240 | return errors.New("subject.commonName: commonName entry not a string") 241 | } 242 | *m = MaybeCommonNameString(vvv) 243 | case nil: 244 | *m = "" 245 | default: 246 | return errors.New("subject.commonName: not a string or map[string]interface{}") 247 | } 248 | 249 | return nil 250 | } 251 | -------------------------------------------------------------------------------- /cpanelgo_test.go: -------------------------------------------------------------------------------- 1 | package cpanelgo 2 | 3 | import ( 4 | "net/url" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestArgs_Values(t *testing.T) { 10 | args := Args{ 11 | "key=not": "value", 12 | } 13 | expected0 := url.Values{ 14 | "key=not": []string{"value"}, 15 | } 16 | actual0 := args.Values("0") 17 | if !reflect.DeepEqual(expected0, actual0) { 18 | t.Errorf("Unexpected Args.Values(), expected: '%+v', got: '%+v'", expected0, actual0) 19 | } 20 | actual1 := args.Values("1") 21 | expected1 := url.Values{ 22 | "key": []string{"not"}, 23 | } 24 | if !reflect.DeepEqual(actual1, expected1) { 25 | t.Errorf("Unexpected Args.Values(), expected: '%+v', got: '%+v'", expected0, actual0) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/cpanelcli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "reflect" 10 | "strings" 11 | 12 | "bufio" 13 | 14 | "io/ioutil" 15 | 16 | "path/filepath" 17 | 18 | "bytes" 19 | 20 | "github.com/letsencrypt-cpanel/cpanelgo" 21 | "github.com/letsencrypt-cpanel/cpanelgo/cpanel" 22 | "github.com/letsencrypt-cpanel/cpanelgo/whm" 23 | ) 24 | 25 | var mode, hostname, username, password, accesshash, impersonate string 26 | var debug, insecure, pretty bool 27 | 28 | var version, module, function string 29 | 30 | func init() { 31 | 32 | flag.Usage = func() { 33 | cmd := filepath.Base(os.Args[0]) 34 | fmt.Fprintf(os.Stderr, "Usage of %s:\n\n", cmd) 35 | fmt.Fprintf(os.Stderr, "cPanel example:\n") 36 | fmt.Fprintf(os.Stderr, "# %s -mode cpanel -hostname 127.0.0.1 -username test -password test -version uapi -module Themes -function get_theme_base\n", cmd) 37 | fmt.Fprintf(os.Stderr, "\tIf password isn't specified, will be prompted\n\n") 38 | fmt.Fprintf(os.Stderr, "WHM impersonation example:\n") 39 | fmt.Fprintf(os.Stderr, "# %s -mode whmimp -hostname 127.0.0.1 -username root -impersonate test -accesshash .accesshash -version uapi -module Themes -function get_theme_base\n\n", cmd) 40 | fmt.Fprintf(os.Stderr, "WHM example:\n") 41 | fmt.Fprintf(os.Stderr, "# %s -mode whm -hostname 127.0.0.1 -username root -accesshash .accesshash -function listaccts\n\n", cmd) 42 | fmt.Fprintf(os.Stderr, "To show extra debug use -debug and to pretty print json result use -pretty\n") 43 | } 44 | 45 | // required flags 46 | flag.StringVar(&mode, "mode", "", "cpanel | whm | whmimp") 47 | 48 | // optional flags 49 | flag.BoolVar(&insecure, "insecure", true, "insecure ssl connection to cpanel/whm") 50 | flag.BoolVar(&debug, "debug", false, "debug cpanel responses") 51 | flag.BoolVar(&pretty, "pretty", false, "pretty cpanel json response") 52 | 53 | // flags for cpanel 54 | flag.StringVar(&password, "password", "", "password for cpanel") 55 | 56 | // flags for cpanel/whm/whmimp 57 | flag.StringVar(&hostname, "hostname", "", "hostname to connect to") 58 | flag.StringVar(&username, "username", "", "username to authenticate") 59 | 60 | // flags for whm/whmimp 61 | flag.StringVar(&accesshash, "accesshash", "", "access hash file path for whm/whmimp") 62 | flag.StringVar(&impersonate, "impersonate", "", "user to impersonate for whmimp") 63 | 64 | // flags for all 65 | flag.StringVar(&version, "version", "", "uapi | 2 | 1") 66 | flag.StringVar(&module, "module", "", "module to run (eg Branding)") 67 | flag.StringVar(&function, "function", "", "function to run (eg include)") 68 | } 69 | 70 | func required(v interface{}, msg string) { 71 | if v == reflect.Zero(reflect.TypeOf(v)).Interface() { 72 | log.Fatal(msg) 73 | } 74 | } 75 | 76 | var modes = map[string]func(){ 77 | "cpanel": modeCpanel, 78 | "whm": modeWhm, 79 | "whmimp": modeWhmImp, 80 | } 81 | 82 | func ifpanic(err error) { 83 | if err != nil { 84 | panic(err) 85 | } 86 | } 87 | 88 | func main() { 89 | flag.Parse() 90 | 91 | required(mode, "Please specify a mode") 92 | required(hostname, "Please specify a hostname") 93 | 94 | if debug { 95 | os.Setenv("DEBUG_CPANEL_RESPONSES", "1") 96 | } 97 | 98 | f, ok := modes[mode] 99 | if !ok { 100 | log.Fatal("Unknown mode:", mode) 101 | } 102 | 103 | f() 104 | } 105 | 106 | func getArgs() cpanelgo.Args { 107 | var args = cpanelgo.Args{} 108 | 109 | if flag.NArg() > 0 { 110 | for _, a := range flag.Args() { 111 | kv := strings.SplitN(a, "=", 2) 112 | if len(kv) == 1 { 113 | args[kv[0]] = "" 114 | } else if len(kv) == 2 { 115 | args[kv[0]] = kv[1] 116 | } 117 | } 118 | } 119 | 120 | return args 121 | } 122 | 123 | func modeCpanel() { 124 | required(username, "Please specify a username") 125 | if password == "" { 126 | fmt.Print("Please enter password: ") 127 | password, _ = bufio.NewReader(os.Stdin).ReadString('\n') 128 | password = strings.Trim(password, "\n") 129 | } 130 | required(password, "Please specify a password") 131 | required(version, "Please specify an api version") 132 | 133 | cl, err := cpanel.NewJsonApi(hostname, username, password, insecure) 134 | ifpanic(err) 135 | 136 | /* 137 | theme, err := cl.GetTheme() 138 | log.Println(theme, err) 139 | parked, err := cl.ListParkedDomains() 140 | log.Println(parked, err) 141 | cl.SetVar("dprefix", "../") 142 | cl.SetVar("hidehelp", "1") 143 | branding, err := cl.BrandingInclude("stdheader.html") 144 | log.Println(branding, err) 145 | */ 146 | 147 | api(cl) 148 | } 149 | 150 | func modeWhmImp() { 151 | required(username, "Please specify a username") 152 | required(impersonate, "Please specify a user to impersonate") 153 | required(accesshash, "Please specify an access hash file") 154 | required(version, "Please specify an api version") 155 | 156 | ahBytes, err := ioutil.ReadFile(accesshash) 157 | ifpanic(err) 158 | if len(ahBytes) == 0 { 159 | log.Fatal("accesshash file was empty") 160 | } 161 | 162 | cl := whm.NewWhmImpersonationApi(hostname, username, string(ahBytes), impersonate, insecure) 163 | 164 | /* 165 | theme, err := cl.GetTheme() 166 | log.Println(theme, err) 167 | parked, err := cl.ListParkedDomains() 168 | log.Println(parked, err) 169 | branding, err := cl.BrandingInclude("stdheader.html") 170 | log.Println(branding, err) 171 | */ 172 | 173 | api(cl) 174 | } 175 | 176 | func api(cl cpanel.CpanelApi) { 177 | var out json.RawMessage 178 | switch version { 179 | case "uapi": 180 | err := cl.Gateway.UAPI(module, function, getArgs(), &out) 181 | ifpanic(err) 182 | printResult(out) 183 | var response cpanelgo.BaseUAPIResponse 184 | err = json.Unmarshal(out, &response) 185 | ifpanic(err) 186 | ifpanic(response.Error()) 187 | fmt.Printf("%+v", response) 188 | case "2": 189 | err := cl.Gateway.API2(module, function, getArgs(), &out) 190 | ifpanic(err) 191 | printResult(out) 192 | var response cpanelgo.BaseAPI2Response 193 | err = json.Unmarshal(out, &response) 194 | ifpanic(err) 195 | ifpanic(response.Error()) 196 | fmt.Printf("%+v", response) 197 | case "1": 198 | err := cl.Gateway.API1(module, function, flag.Args(), &out) 199 | ifpanic(err) 200 | printResult(out) 201 | var response cpanelgo.BaseAPI1Response 202 | err = json.Unmarshal(out, &response) 203 | ifpanic(err) 204 | ifpanic(response.Error()) 205 | fmt.Printf("%+v", response) 206 | default: 207 | log.Fatalf("Unknown version: %q, expected uapi, 2 or 1", version) 208 | } 209 | } 210 | 211 | func modeWhm() { 212 | required(username, "Please specify a username") 213 | required(accesshash, "Please specify an access hash file") 214 | 215 | ahBytes, err := ioutil.ReadFile(accesshash) 216 | ifpanic(err) 217 | if len(ahBytes) == 0 { 218 | log.Fatal("accesshash file was empty") 219 | } 220 | 221 | whmcl := whm.NewWhmApiAccessHash(hostname, username, string(ahBytes), insecure) 222 | ifpanic(err) 223 | 224 | var out json.RawMessage 225 | err = whmcl.WHMAPI1(function, getArgs(), &out) 226 | ifpanic(err) 227 | 228 | printResult(out) 229 | } 230 | 231 | func printResult(out json.RawMessage) { 232 | if pretty { 233 | var pretty bytes.Buffer 234 | json.Indent(&pretty, out, "", "\t") 235 | 236 | fmt.Println(string(pretty.Bytes())) 237 | } else { 238 | fmt.Println(string(out)) 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/letsencrypt-cpanel/cpanelgo 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /whm/account.go: -------------------------------------------------------------------------------- 1 | package whm 2 | 3 | import "github.com/letsencrypt-cpanel/cpanelgo" 4 | 5 | type ListAccountsApiResponse struct { 6 | BaseWhmApiResponse 7 | Data struct { 8 | Accounts []struct { 9 | User string `json:"user"` 10 | } `json:"acct"` 11 | } `json:"data"` 12 | } 13 | 14 | func (a WhmApi) ListAccounts() (ListAccountsApiResponse, error) { 15 | var out ListAccountsApiResponse 16 | 17 | err := a.WHMAPI1("listaccts", cpanelgo.Args{}, &out) 18 | if err == nil { 19 | err = out.Error() 20 | } 21 | 22 | return out, err 23 | } 24 | 25 | type AccountSummaryApiResponse struct { 26 | BaseWhmApiResponse 27 | Data struct { 28 | Account []struct { 29 | Email string `json:"email"` 30 | Suspended int `json:"suspended"` 31 | } `json:"acct"` 32 | } `json:"data"` 33 | } 34 | 35 | func (r AccountSummaryApiResponse) HasEmail() bool { 36 | e := r.Email() 37 | return e != "" && e != "*unknown*" 38 | } 39 | 40 | func (r AccountSummaryApiResponse) Email() string { 41 | for _, v := range r.Data.Account { 42 | if v.Email != "" { 43 | return v.Email 44 | } 45 | } 46 | return "" 47 | } 48 | 49 | func (r AccountSummaryApiResponse) Suspended() bool { 50 | for _, v := range r.Data.Account { 51 | if v.Suspended != 0 { 52 | return true 53 | } 54 | } 55 | return false 56 | } 57 | 58 | func (a WhmApi) AccountSummary(username string) (AccountSummaryApiResponse, error) { 59 | var out AccountSummaryApiResponse 60 | 61 | err := a.WHMAPI1("accountsummary", cpanelgo.Args{ 62 | "user": username, 63 | }, &out) 64 | if err == nil { 65 | err = out.Error() 66 | } 67 | 68 | return out, err 69 | } 70 | 71 | type ResolveDomainNameApiResponse struct { 72 | BaseWhmApiResponse 73 | Data struct { 74 | IP string `json:"ip"` 75 | } `json:"data"` 76 | } 77 | 78 | func (a WhmApi) ResolveDomainName(domain string) (ResolveDomainNameApiResponse, error) { 79 | var out ResolveDomainNameApiResponse 80 | 81 | err := a.WHMAPI1("resolvedomainname", cpanelgo.Args{ 82 | "domain": domain, 83 | }, &out) 84 | if err == nil { 85 | err = out.Error() 86 | } 87 | 88 | return out, err 89 | } 90 | -------------------------------------------------------------------------------- /whm/impersonation.go: -------------------------------------------------------------------------------- 1 | package whm 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "encoding/json" 8 | 9 | "github.com/letsencrypt-cpanel/cpanelgo" 10 | "github.com/letsencrypt-cpanel/cpanelgo/cpanel" 11 | ) 12 | 13 | type WhmImpersonationApi struct { 14 | Impersonate string // who we are impersonating 15 | WhmApi 16 | } 17 | 18 | func NewWhmImpersonationApi(hostname, username, accessHash, userToImpersonate string, insecure bool) cpanel.CpanelApi { 19 | accessHash = strings.Replace(accessHash, "\n", "", -1) 20 | accessHash = strings.Replace(accessHash, "\r", "", -1) 21 | 22 | return cpanel.CpanelApi{cpanelgo.NewApi( 23 | &WhmImpersonationApi{ 24 | Impersonate: userToImpersonate, 25 | WhmApi: WhmApi{ 26 | Hostname: hostname, 27 | Username: username, 28 | AccessHash: accessHash, 29 | Insecure: insecure, 30 | }, 31 | })} 32 | } 33 | 34 | func NewWhmImpersonationApiWithClient(hostname, username, accessHash, userToImpersonate string, insecure bool, cl *http.Client) cpanel.CpanelApi { 35 | accessHash = strings.Replace(accessHash, "\n", "", -1) 36 | accessHash = strings.Replace(accessHash, "\r", "", -1) 37 | 38 | return cpanel.CpanelApi{cpanelgo.NewApi( 39 | &WhmImpersonationApi{ 40 | Impersonate: userToImpersonate, 41 | WhmApi: WhmApi{ 42 | Hostname: hostname, 43 | Username: username, 44 | AccessHash: accessHash, 45 | Insecure: insecure, 46 | cl: cl, 47 | }, 48 | })} 49 | } 50 | 51 | func NewWhmImpersonationApiTotp(hostname, username, accessHash, userToImpersonate, secret string, insecure bool) cpanel.CpanelApi { 52 | accessHash = strings.Replace(accessHash, "\n", "", -1) 53 | accessHash = strings.Replace(accessHash, "\r", "", -1) 54 | 55 | return cpanel.CpanelApi{cpanelgo.NewApi( 56 | &WhmImpersonationApi{ 57 | Impersonate: userToImpersonate, 58 | WhmApi: WhmApi{ 59 | Hostname: hostname, 60 | Username: username, 61 | AccessHash: accessHash, 62 | Insecure: insecure, 63 | TotpSecret: secret, 64 | }, 65 | })} 66 | } 67 | 68 | func (c *WhmImpersonationApi) UAPI(module, function string, arguments cpanelgo.Args, out interface{}) error { 69 | if arguments == nil { 70 | arguments = cpanelgo.Args{} 71 | } 72 | arguments["user"] = c.Impersonate 73 | arguments["cpanel_jsonapi_user"] = c.Impersonate 74 | arguments["cpanel_jsonapi_apiversion"] = "3" 75 | arguments["cpanel_jsonapi_module"] = module 76 | arguments["cpanel_jsonapi_func"] = function 77 | 78 | var result cpanelgo.UAPIResult 79 | err := c.WHMAPI1("cpanel", arguments, &result) 80 | if err == nil { 81 | err = result.Error() 82 | } 83 | if err != nil { 84 | return err 85 | } 86 | return json.Unmarshal(result.Result, out) 87 | } 88 | 89 | func (c *WhmImpersonationApi) API2(module, function string, arguments cpanelgo.Args, out interface{}) error { 90 | if arguments == nil { 91 | arguments = cpanelgo.Args{} 92 | } 93 | arguments["user"] = c.Impersonate 94 | arguments["cpanel_jsonapi_user"] = c.Impersonate 95 | arguments["cpanel_jsonapi_apiversion"] = "2" 96 | arguments["cpanel_jsonapi_module"] = module 97 | arguments["cpanel_jsonapi_func"] = function 98 | 99 | var result cpanelgo.API2Result 100 | err := c.WHMAPI1("cpanel", arguments, &result) 101 | if err == nil { 102 | err = result.Error() 103 | } 104 | if err != nil { 105 | return err 106 | } 107 | return json.Unmarshal(result.Result, out) 108 | } 109 | 110 | func (c *WhmImpersonationApi) API1(module, function string, arguments []string, out interface{}) error { 111 | args := cpanelgo.Args{} 112 | args["user"] = c.Impersonate 113 | args["cpanel_jsonapi_user"] = c.Impersonate 114 | args["cpanel_jsonapi_apiversion"] = "1" 115 | args["cpanel_jsonapi_module"] = module 116 | args["cpanel_jsonapi_func"] = function 117 | 118 | if arguments != nil && len(arguments) > 0 { 119 | for _, v := range arguments { 120 | args[v] = true 121 | } 122 | } 123 | 124 | return c.WHMAPI1("cpanel", args, out) 125 | } 126 | 127 | func (c *WhmImpersonationApi) Close() error { 128 | return nil 129 | } 130 | -------------------------------------------------------------------------------- /whm/servicessl.go: -------------------------------------------------------------------------------- 1 | package whm 2 | 3 | import "github.com/letsencrypt-cpanel/cpanelgo" 4 | 5 | func (a WhmApi) InstallServiceSslCertificate(service, crt, key, cabundle string) (BaseWhmApiResponse, error) { 6 | var out BaseWhmApiResponse 7 | 8 | err := a.WHMAPI1("install_service_ssl_certificate", cpanelgo.Args{ 9 | "service": service, 10 | "crt": crt, 11 | "key": key, 12 | "cabundle": cabundle, 13 | }, &out) 14 | if err == nil { 15 | err = out.Error() 16 | } 17 | 18 | return out, err 19 | } 20 | 21 | type FetchServiceSslComponentsAPIResponse struct { 22 | BaseWhmApiResponse 23 | Data struct { 24 | Services []struct { 25 | Service string `json:"service"` 26 | Certificate string `json:"certificate"` 27 | CABundle string `json:"cabundle"` 28 | CertificateInfo struct { 29 | IsSelfSigned int `json:"is_self_signed"` 30 | NotAfter cpanelgo.MaybeInt64 `json:"not_after"` 31 | Domains []string `json:"domains"` 32 | } `json:"certificate_info"` 33 | } `json:"services"` 34 | } `json:"data"` 35 | } 36 | 37 | func (r FetchServiceSslComponentsAPIResponse) Services() []string { 38 | out := []string{} 39 | for _, v := range r.Data.Services { 40 | out = append(out, v.Service) 41 | } 42 | return out 43 | } 44 | 45 | func (a WhmApi) FetchServiceSslComponents() (FetchServiceSslComponentsAPIResponse, error) { 46 | var out FetchServiceSslComponentsAPIResponse 47 | 48 | err := a.WHMAPI1("fetch_service_ssl_components", cpanelgo.Args{}, &out) 49 | if err == nil { 50 | err = out.Error() 51 | } 52 | 53 | return out, err 54 | } 55 | 56 | func (a WhmApi) RestartService(name string) (BaseWhmApiResponse, error) { 57 | var out BaseWhmApiResponse 58 | 59 | err := a.WHMAPI1("restartservice", cpanelgo.Args{ 60 | "service": name, 61 | }, &out) 62 | if err == nil { 63 | err = out.Error() 64 | } 65 | 66 | return out, err 67 | } 68 | -------------------------------------------------------------------------------- /whm/ssl.go: -------------------------------------------------------------------------------- 1 | package whm 2 | 3 | import ( 4 | "github.com/letsencrypt-cpanel/cpanelgo" 5 | "github.com/letsencrypt-cpanel/cpanelgo/cpanel" 6 | ) 7 | 8 | type VhostEntry struct { 9 | User string `json:"user"` 10 | Docroot string `json:"docroot"` 11 | Certificate cpanel.CpanelSslCertificate `json:"crt"` 12 | } 13 | 14 | type FetchSslVhostsApiResponse struct { 15 | BaseWhmApiResponse 16 | Data struct { 17 | Vhosts []VhostEntry `json:"vhosts"` 18 | } `json:"data"` 19 | } 20 | 21 | func (a WhmApi) FetchSslVhosts() (FetchSslVhostsApiResponse, error) { 22 | var out FetchSslVhostsApiResponse 23 | 24 | err := a.WHMAPI1("fetch_ssl_vhosts", cpanelgo.Args{}, &out) 25 | if err == nil { 26 | err = out.Error() 27 | } 28 | 29 | return out, err 30 | } 31 | -------------------------------------------------------------------------------- /whm/tweak.go: -------------------------------------------------------------------------------- 1 | package whm 2 | 3 | import "github.com/letsencrypt-cpanel/cpanelgo" 4 | 5 | type GetTweakSettingApiResponse struct { 6 | BaseWhmApiResponse 7 | Data struct { 8 | TweakSetting struct { 9 | Key string `json:"key"` 10 | Value string `json:"value"` 11 | } `json:"tweaksetting"` 12 | } `json:"data"` 13 | } 14 | 15 | func (a WhmApi) GetTweakSetting(key, module string) (GetTweakSettingApiResponse, error) { 16 | var out GetTweakSettingApiResponse 17 | 18 | err := a.WHMAPI1("get_tweaksetting", cpanelgo.Args{ 19 | "key": key, 20 | "module": module, 21 | }, &out) 22 | if err == nil { 23 | err = out.Error() 24 | } 25 | 26 | return out, err 27 | } 28 | 29 | func (a WhmApi) SetTweakSetting(key, module, value string) (BaseWhmApiResponse, error) { 30 | var out BaseWhmApiResponse 31 | 32 | err := a.WHMAPI1("set_tweaksetting", cpanelgo.Args{ 33 | "key": key, 34 | "module": module, 35 | "value": value, 36 | }, &out) 37 | if err == nil { 38 | err = out.Error() 39 | } 40 | 41 | return out, err 42 | } 43 | -------------------------------------------------------------------------------- /whm/user.go: -------------------------------------------------------------------------------- 1 | package whm 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/letsencrypt-cpanel/cpanelgo" 7 | ) 8 | 9 | type CreateUserSessionApiResponse struct { 10 | BaseWhmApiResponse 11 | Data struct { 12 | SecurityToken string `json:"cp_security_token"` 13 | Expires cpanelgo.MaybeInt64 `json:"expires"` 14 | Session string `json:"session"` 15 | Url string `json:"url"` 16 | } `json:"data"` 17 | } 18 | 19 | func (a WhmApi) CreateUserSession(username, service string) (CreateUserSessionApiResponse, error) { 20 | var out CreateUserSessionApiResponse 21 | 22 | err := a.WHMAPI1("create_user_session", cpanelgo.Args{ 23 | "user": username, 24 | "service": service, 25 | }, &out) 26 | if err == nil && out.Result() != 1 { 27 | err = errors.New(out.Metadata.Reason) 28 | } 29 | 30 | return out, err 31 | } 32 | -------------------------------------------------------------------------------- /whm/whm.go: -------------------------------------------------------------------------------- 1 | package whm 2 | 3 | import ( 4 | "bytes" 5 | "crypto/hmac" 6 | "crypto/tls" 7 | "encoding/binary" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "hash" 12 | "io" 13 | "io/ioutil" 14 | "log" 15 | "net/http" 16 | "os" 17 | "strconv" 18 | "strings" 19 | 20 | "encoding/base64" 21 | 22 | "time" 23 | 24 | "crypto/sha1" 25 | "encoding/base32" 26 | 27 | "github.com/letsencrypt-cpanel/cpanelgo" 28 | ) 29 | 30 | type BaseWhmApiResponse struct { 31 | Metadata struct { 32 | Reason string `json:"reason"` 33 | ResultRaw interface{} `json:"result"` 34 | } `json:"metadata"` 35 | } 36 | 37 | func (r BaseWhmApiResponse) Error() error { 38 | if r.Result() == 1 { 39 | return nil 40 | } 41 | if len(r.Metadata.Reason) == 0 { 42 | return errors.New("Unknown") 43 | } 44 | return errors.New(r.Metadata.Reason) 45 | } 46 | 47 | // WHM randomly returns this as a string, gg 48 | func (r BaseWhmApiResponse) Result() int { 49 | if v, ok := r.Metadata.ResultRaw.(float64); ok { // default for Number JSON type is f64 50 | return int(v) 51 | } 52 | 53 | if s, ok := r.Metadata.ResultRaw.(string); ok { 54 | if v, err := strconv.Atoi(s); err != nil { 55 | return -1 56 | } else { 57 | return v 58 | } 59 | } 60 | 61 | return -1 62 | } 63 | 64 | // This implements a standalone WHM client, not for the cPanel API 65 | type WhmApi struct { 66 | Hostname string 67 | Username string 68 | AccessHash string 69 | Password string 70 | Insecure bool 71 | TotpSecret string 72 | cl *http.Client 73 | } 74 | 75 | func NewWhmApiAccessHash(hostname, username, accessHash string, insecure bool) WhmApi { 76 | accessHash = strings.Replace(accessHash, "\n", "", -1) 77 | accessHash = strings.Replace(accessHash, "\r", "", -1) 78 | 79 | return WhmApi{ 80 | Hostname: hostname, 81 | Username: username, 82 | AccessHash: accessHash, 83 | Insecure: insecure, 84 | } 85 | } 86 | 87 | func NewWhmApiAccessHashWithClient(hostname, username, accessHash string, insecure bool, cl *http.Client) WhmApi { 88 | accessHash = strings.Replace(accessHash, "\n", "", -1) 89 | accessHash = strings.Replace(accessHash, "\r", "", -1) 90 | 91 | return WhmApi{ 92 | Hostname: hostname, 93 | Username: username, 94 | AccessHash: accessHash, 95 | Insecure: insecure, 96 | cl: cl, 97 | } 98 | } 99 | 100 | func NewWhmApiAccessHashTotp(hostname, username, accessHash string, insecure bool, secret string) WhmApi { 101 | accessHash = strings.Replace(accessHash, "\n", "", -1) 102 | accessHash = strings.Replace(accessHash, "\r", "", -1) 103 | 104 | return WhmApi{ 105 | Hostname: hostname, 106 | Username: username, 107 | AccessHash: accessHash, 108 | Insecure: insecure, 109 | TotpSecret: secret, 110 | } 111 | } 112 | 113 | func NewWhmApiPassword(hostname, username, password string, insecure bool) WhmApi { 114 | return WhmApi{ 115 | Hostname: hostname, 116 | Username: username, 117 | Password: password, 118 | Insecure: insecure, 119 | } 120 | } 121 | 122 | // Force POST method for these WHM API1 functions 123 | var forcePost = map[string]bool{ 124 | "cpanel": true, 125 | } 126 | 127 | func (c *WhmApi) WHMAPI1(function string, arguments cpanelgo.Args, out interface{}) error { 128 | if c.cl == nil { 129 | c.cl = &http.Client{} 130 | c.cl.Transport = &http.Transport{ 131 | DisableKeepAlives: true, 132 | MaxIdleConns: 1, 133 | MaxIdleConnsPerHost: 1, 134 | TLSClientConfig: &tls.Config{ 135 | InsecureSkipVerify: c.Insecure, 136 | }, 137 | } 138 | } 139 | 140 | method := "GET" 141 | if _, ok := forcePost[function]; ok { 142 | method = "POST" 143 | } 144 | 145 | version := "0" 146 | if arguments["cpanel_jsonapi_apiversion"] == "1" { 147 | version = "1" 148 | } 149 | vals := arguments.Values(version) 150 | vals.Set("api.version", "1") 151 | 152 | var req *http.Request 153 | var reqUrl string 154 | var err error 155 | 156 | if method == "GET" { 157 | reqUrl = fmt.Sprintf("https://%s:2087/json-api/%s?%s", c.Hostname, function, vals.Encode()) 158 | req, err = http.NewRequest("GET", reqUrl, nil) 159 | if err != nil { 160 | return err 161 | } 162 | 163 | } else if method == "POST" { 164 | reqUrl = fmt.Sprintf("https://%s:2087/json-api/%s", c.Hostname, function) 165 | req, err = http.NewRequest("POST", reqUrl, strings.NewReader(vals.Encode())) 166 | } 167 | 168 | if c.AccessHash != "" { 169 | req.Header.Add("Authorization", fmt.Sprintf("WHM %s:%s", c.Username, c.AccessHash)) 170 | } else if c.Password != "" { 171 | req.Header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", c.Username, c.Password)))) 172 | } 173 | 174 | if c.TotpSecret != "" { 175 | decodedSecret, _ := base32.StdEncoding.DecodeString(c.TotpSecret) 176 | otp, _ := totp(decodedSecret, time.Now().Unix(), sha1.New, 6) 177 | 178 | req.Header.Add("X-CPANEL-OTP", otp) 179 | } 180 | 181 | resp, err := c.cl.Do(req) 182 | if err != nil { 183 | return err 184 | } 185 | defer resp.Body.Close() 186 | 187 | if resp.StatusCode >= 300 { 188 | return errors.New(resp.Status) 189 | } 190 | 191 | // limit maximum response size 192 | lReader := io.LimitReader(resp.Body, int64(cpanelgo.ResponseSizeLimit)) 193 | 194 | bytes, err := ioutil.ReadAll(lReader) 195 | if err != nil { 196 | return err 197 | } 198 | 199 | if os.Getenv("DEBUG_CPANEL_RESPONSES") == "1" { 200 | log.Println(reqUrl) 201 | log.Println(resp.Status) 202 | log.Println(function) 203 | log.Println(arguments) 204 | log.Println(string(bytes)) 205 | } 206 | 207 | if len(bytes) == cpanelgo.ResponseSizeLimit { 208 | return errors.New("API response maximum size exceeded") 209 | } 210 | 211 | return json.Unmarshal(bytes, out) 212 | } 213 | 214 | type VersionApiResponse struct { 215 | BaseWhmApiResponse 216 | Data struct { 217 | Version string `json:"version"` 218 | } `json:"data"` 219 | } 220 | 221 | func (a WhmApi) Version() (VersionApiResponse, error) { 222 | var out VersionApiResponse 223 | err := a.WHMAPI1("version", cpanelgo.Args{}, &out) 224 | if err == nil && out.Result() != 1 { 225 | err = out.Error() 226 | } 227 | return out, err 228 | } 229 | 230 | func totp(k []byte, t int64, h func() hash.Hash, l int64) (string, error) { 231 | if l > 9 || l < 1 { 232 | return "", errors.New("Totp: Length out of range.") 233 | } 234 | 235 | time := new(bytes.Buffer) 236 | 237 | err := binary.Write(time, binary.BigEndian, (t-int64(0))/int64(30)) 238 | if err != nil { 239 | return "", err 240 | } 241 | 242 | hash := hmac.New(h, k) 243 | hash.Write(time.Bytes()) 244 | v := hash.Sum(nil) 245 | 246 | o := v[len(v)-1] & 0xf 247 | c := (int32(v[o]&0x7f)<<24 | int32(v[o+1])<<16 | int32(v[o+2])<<8 | int32(v[o+3])) % 1000000000 248 | 249 | return fmt.Sprintf("%010d", c)[10-l : 10], nil 250 | } 251 | --------------------------------------------------------------------------------