package joker import ( "fmt" "io/ioutil" "net/http" "net/url" "strconv" "strings" "github.com/go-acme/lego/v3/challenge/dns01" "github.com/go-acme/lego/v3/log" ) const defaultBaseURL = "https://dmapi.joker.com/request/" // Joker DMAPI Response type response struct { Headers url.Values Body string StatusCode int StatusText string AuthSid string } // parseResponse parses HTTP response body func parseResponse(message string) *response { r := &response{Headers: url.Values{}, StatusCode: -1} parts := strings.SplitN(message, "\n\n", 2) for _, line := range strings.Split(parts[0], "\n") { if strings.TrimSpace(line) == "" { continue } kv := strings.SplitN(line, ":", 2) val := "" if len(kv) == 2 { val = strings.TrimSpace(kv[1]) } r.Headers.Add(kv[0], val) switch kv[0] { case "Status-Code": i, err := strconv.Atoi(val) if err == nil { r.StatusCode = i } case "Status-Text": r.StatusText = val case "Auth-Sid": r.AuthSid = val } } if len(parts) > 1 { r.Body = parts[1] } return r } // login performs a login to Joker's DMAPI func (d *DNSProvider) login() (*response, error) { if d.config.AuthSid != "" { // already logged in return nil, nil } var values url.Values switch { case d.config.Username != "" && d.config.Password != "": values = url.Values{ "username": {d.config.Username}, "password": {d.config.Password}, } case d.config.APIKey != "": values = url.Values{"api-key": {d.config.APIKey}} default: return nil, fmt.Errorf("no username and password or api-key") } response, err := d.postRequest("login", values) if err != nil { return response, err } if response == nil { return nil, fmt.Errorf("login returned nil response") } if response.AuthSid == "" { return response, fmt.Errorf("login did not return valid Auth-Sid") } d.config.AuthSid = response.AuthSid return response, nil } // logout closes authenticated session with Joker's DMAPI func (d *DNSProvider) logout() (*response, error) { if d.config.AuthSid == "" { return nil, fmt.Errorf("already logged out") } response, err := d.postRequest("logout", url.Values{}) if err == nil { d.config.AuthSid = "" } return response, err } // getZone returns content of DNS zone for domain func (d *DNSProvider) getZone(domain string) (*response, error) { if d.config.AuthSid == "" { return nil, fmt.Errorf("must be logged in to get zone") } return d.postRequest("dns-zone-get", url.Values{"domain": {dns01.UnFqdn(domain)}}) } // putZone uploads DNS zone to Joker DMAPI func (d *DNSProvider) putZone(domain, zone string) (*response, error) { if d.config.AuthSid == "" { return nil, fmt.Errorf("must be logged in to put zone") } return d.postRequest("dns-zone-put", url.Values{"domain": {dns01.UnFqdn(domain)}, "zone": {strings.TrimSpace(zone)}}) } // postRequest performs actual HTTP request func (d *DNSProvider) postRequest(cmd string, data url.Values) (*response, error) { uri := d.config.BaseURL + cmd if d.config.AuthSid != "" { data.Set("auth-sid", d.config.AuthSid) } if d.config.Debug { log.Infof("postRequest:\n\tURL: %q\n\tData: %v", uri, data) } resp, err := d.config.HTTPClient.PostForm(uri, data) if err != nil { return nil, err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != 200 { return nil, fmt.Errorf("HTTP error %d [%s]: %v", resp.StatusCode, http.StatusText(resp.StatusCode), string(body)) } return parseResponse(string(body)), nil } // Temporary workaround, until it get fixed on API side func fixTxtLines(line string) string { fields := strings.Fields(line) if len(fields) < 6 || fields[1] != "TXT" { return line } if fields[3][0] == '"' && fields[4] == `"` { fields[3] = strings.TrimSpace(fields[3]) + `"` fields = append(fields[:4], fields[5:]...) } return strings.Join(fields, " ") } // removeTxtEntryFromZone clean-ups all TXT records with given name func removeTxtEntryFromZone(zone, relative string) (string, bool) { prefix := fmt.Sprintf("%s TXT 0 ", relative) modified := false var zoneEntries []string for _, line := range strings.Split(zone, "\n") { if strings.HasPrefix(line, prefix) { modified = true continue } zoneEntries = append(zoneEntries, line) } return strings.TrimSpace(strings.Join(zoneEntries, "\n")), modified } // addTxtEntryToZone returns DNS zone with added TXT record func addTxtEntryToZone(zone, relative, value string, ttl int) string { var zoneEntries []string for _, line := range strings.Split(zone, "\n") { zoneEntries = append(zoneEntries, fixTxtLines(line)) } newZoneEntry := fmt.Sprintf("%s TXT 0 %q %d", relative, value, ttl) zoneEntries = append(zoneEntries, newZoneEntry) return strings.TrimSpace(strings.Join(zoneEntries, "\n")) }