1package gandi 2 3import ( 4 "bytes" 5 "encoding/xml" 6 "errors" 7 "fmt" 8 "io" 9) 10 11// types for XML-RPC method calls and parameters 12 13type param interface { 14 param() 15} 16 17type paramString struct { 18 XMLName xml.Name `xml:"param"` 19 Value string `xml:"value>string"` 20} 21 22type paramInt struct { 23 XMLName xml.Name `xml:"param"` 24 Value int `xml:"value>int"` 25} 26 27type structMember interface { 28 structMember() 29} 30 31type structMemberString struct { 32 Name string `xml:"name"` 33 Value string `xml:"value>string"` 34} 35 36type structMemberInt struct { 37 Name string `xml:"name"` 38 Value int `xml:"value>int"` 39} 40 41type paramStruct struct { 42 XMLName xml.Name `xml:"param"` 43 StructMembers []structMember `xml:"value>struct>member"` 44} 45 46func (p paramString) param() {} 47func (p paramInt) param() {} 48func (m structMemberString) structMember() {} 49func (m structMemberInt) structMember() {} 50func (p paramStruct) param() {} 51 52type methodCall struct { 53 XMLName xml.Name `xml:"methodCall"` 54 MethodName string `xml:"methodName"` 55 Params []param `xml:"params"` 56} 57 58// types for XML-RPC responses 59 60type response interface { 61 faultCode() int 62 faultString() string 63} 64 65type responseFault struct { 66 FaultCode int `xml:"fault>value>struct>member>value>int"` 67 FaultString string `xml:"fault>value>struct>member>value>string"` 68} 69 70func (r responseFault) faultCode() int { return r.FaultCode } 71func (r responseFault) faultString() string { return r.FaultString } 72 73type responseStruct struct { 74 responseFault 75 StructMembers []struct { 76 Name string `xml:"name"` 77 ValueInt int `xml:"value>int"` 78 } `xml:"params>param>value>struct>member"` 79} 80 81type responseInt struct { 82 responseFault 83 Value int `xml:"params>param>value>int"` 84} 85 86type responseBool struct { 87 responseFault 88 Value bool `xml:"params>param>value>boolean"` 89} 90 91type rpcError struct { 92 faultCode int 93 faultString string 94} 95 96func (e rpcError) Error() string { 97 return fmt.Sprintf("Gandi DNS: RPC Error: (%d) %s", e.faultCode, e.faultString) 98} 99 100// rpcCall makes an XML-RPC call to Gandi's RPC endpoint by 101// marshaling the data given in the call argument to XML and sending 102// that via HTTP Post to Gandi. 103// The response is then unmarshalled into the resp argument. 104func (d *DNSProvider) rpcCall(call *methodCall, resp response) error { 105 // marshal 106 b, err := xml.MarshalIndent(call, "", " ") 107 if err != nil { 108 return fmt.Errorf("marshal error: %w", err) 109 } 110 111 // post 112 b = append([]byte(`<?xml version="1.0"?>`+"\n"), b...) 113 respBody, err := d.httpPost(d.config.BaseURL, "text/xml", bytes.NewReader(b)) 114 if err != nil { 115 return err 116 } 117 118 // unmarshal 119 err = xml.Unmarshal(respBody, resp) 120 if err != nil { 121 return fmt.Errorf("unmarshal error: %w", err) 122 } 123 if resp.faultCode() != 0 { 124 return rpcError{ 125 faultCode: resp.faultCode(), faultString: resp.faultString(), 126 } 127 } 128 return nil 129} 130 131// functions to perform API actions 132 133func (d *DNSProvider) getZoneID(domain string) (int, error) { 134 resp := &responseStruct{} 135 err := d.rpcCall(&methodCall{ 136 MethodName: "domain.info", 137 Params: []param{ 138 paramString{Value: d.config.APIKey}, 139 paramString{Value: domain}, 140 }, 141 }, resp) 142 if err != nil { 143 return 0, err 144 } 145 146 var zoneID int 147 for _, member := range resp.StructMembers { 148 if member.Name == "zone_id" { 149 zoneID = member.ValueInt 150 } 151 } 152 153 if zoneID == 0 { 154 return 0, fmt.Errorf("could not determine zone_id for %s", domain) 155 } 156 return zoneID, nil 157} 158 159func (d *DNSProvider) cloneZone(zoneID int, name string) (int, error) { 160 resp := &responseStruct{} 161 err := d.rpcCall(&methodCall{ 162 MethodName: "domain.zone.clone", 163 Params: []param{ 164 paramString{Value: d.config.APIKey}, 165 paramInt{Value: zoneID}, 166 paramInt{Value: 0}, 167 paramStruct{ 168 StructMembers: []structMember{ 169 structMemberString{ 170 Name: "name", 171 Value: name, 172 }, 173 }, 174 }, 175 }, 176 }, resp) 177 if err != nil { 178 return 0, err 179 } 180 181 var newZoneID int 182 for _, member := range resp.StructMembers { 183 if member.Name == "id" { 184 newZoneID = member.ValueInt 185 } 186 } 187 188 if newZoneID == 0 { 189 return 0, errors.New("could not determine cloned zone_id") 190 } 191 return newZoneID, nil 192} 193 194func (d *DNSProvider) newZoneVersion(zoneID int) (int, error) { 195 resp := &responseInt{} 196 err := d.rpcCall(&methodCall{ 197 MethodName: "domain.zone.version.new", 198 Params: []param{ 199 paramString{Value: d.config.APIKey}, 200 paramInt{Value: zoneID}, 201 }, 202 }, resp) 203 if err != nil { 204 return 0, err 205 } 206 207 if resp.Value == 0 { 208 return 0, errors.New("could not create new zone version") 209 } 210 return resp.Value, nil 211} 212 213func (d *DNSProvider) addTXTRecord(zoneID, version int, name, value string, ttl int) error { 214 resp := &responseStruct{} 215 err := d.rpcCall(&methodCall{ 216 MethodName: "domain.zone.record.add", 217 Params: []param{ 218 paramString{Value: d.config.APIKey}, 219 paramInt{Value: zoneID}, 220 paramInt{Value: version}, 221 paramStruct{ 222 StructMembers: []structMember{ 223 structMemberString{ 224 Name: "type", 225 Value: "TXT", 226 }, structMemberString{ 227 Name: "name", 228 Value: name, 229 }, structMemberString{ 230 Name: "value", 231 Value: value, 232 }, structMemberInt{ 233 Name: "ttl", 234 Value: ttl, 235 }, 236 }, 237 }, 238 }, 239 }, resp) 240 return err 241} 242 243func (d *DNSProvider) setZoneVersion(zoneID, version int) error { 244 resp := &responseBool{} 245 err := d.rpcCall(&methodCall{ 246 MethodName: "domain.zone.version.set", 247 Params: []param{ 248 paramString{Value: d.config.APIKey}, 249 paramInt{Value: zoneID}, 250 paramInt{Value: version}, 251 }, 252 }, resp) 253 if err != nil { 254 return err 255 } 256 257 if !resp.Value { 258 return errors.New("could not set zone version") 259 } 260 return nil 261} 262 263func (d *DNSProvider) setZone(domain string, zoneID int) error { 264 resp := &responseStruct{} 265 err := d.rpcCall(&methodCall{ 266 MethodName: "domain.zone.set", 267 Params: []param{ 268 paramString{Value: d.config.APIKey}, 269 paramString{Value: domain}, 270 paramInt{Value: zoneID}, 271 }, 272 }, resp) 273 if err != nil { 274 return err 275 } 276 277 var respZoneID int 278 for _, member := range resp.StructMembers { 279 if member.Name == "zone_id" { 280 respZoneID = member.ValueInt 281 } 282 } 283 284 if respZoneID != zoneID { 285 return fmt.Errorf("could not set new zone_id for %s", domain) 286 } 287 return nil 288} 289 290func (d *DNSProvider) deleteZone(zoneID int) error { 291 resp := &responseBool{} 292 err := d.rpcCall(&methodCall{ 293 MethodName: "domain.zone.delete", 294 Params: []param{ 295 paramString{Value: d.config.APIKey}, 296 paramInt{Value: zoneID}, 297 }, 298 }, resp) 299 if err != nil { 300 return err 301 } 302 303 if !resp.Value { 304 return errors.New("could not delete zone_id") 305 } 306 return nil 307} 308 309func (d *DNSProvider) httpPost(url, bodyType string, body io.Reader) ([]byte, error) { 310 resp, err := d.config.HTTPClient.Post(url, bodyType, body) 311 if err != nil { 312 return nil, fmt.Errorf("HTTP Post Error: %w", err) 313 } 314 defer resp.Body.Close() 315 316 b, err := io.ReadAll(resp.Body) 317 if err != nil { 318 return nil, fmt.Errorf("HTTP Post Error: %w", err) 319 } 320 321 return b, nil 322} 323