gnsregistrar.go (39919B)
1 // This file is part of gnsregistrar, a GNS registrar implementation. 2 // Copyright (C) 2022 Martin Schanzenbach 3 // 4 // gnsregistrar is free software: you can redistribute it and/or modify it 5 // under the terms of the GNU Affero General Public License as published 6 // by the Free Software Foundation, either version 3 of the License, 7 // or (at your option) any later version. 8 // 9 // gnsregistrar is distributed in the hope that it will be useful, but 10 // WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 // Affero General Public License for more details. 13 // 14 // You should have received a copy of the GNU Affero General Public License 15 // along with this program. If not, see <http://www.gnu.org/licenses/>. 16 // 17 // SPDX-License-Identifier: AGPL3.0-or-later 18 19 package gnsregistrar 20 21 import ( 22 "bytes" 23 "encoding/base64" 24 "encoding/json" 25 "errors" 26 "fmt" 27 "html/template" 28 "io" 29 "log" 30 "net/http" 31 "net/url" 32 "os" 33 "os/exec" 34 "regexp" 35 "strings" 36 "time" 37 38 "github.com/google/uuid" 39 "github.com/gorilla/mux" 40 "github.com/schanzen/taler-go/pkg/merchant" 41 talerutil "github.com/schanzen/taler-go/pkg/util" 42 "github.com/skip2/go-qrcode" 43 "gopkg.in/ini.v1" 44 ) 45 46 type RegistrarConfig struct { 47 // The config to use 48 Ini *ini.File 49 50 // The Service Version 51 Version string 52 53 // The data home location (usually $datadir/gnunet-gns-registrar) 54 Datahome string 55 56 // The merchant connection to use 57 Merchant merchant.Merchant 58 59 // The loglevel to use 60 Loglevel LogLevel 61 } 62 63 // This is metadata stored in the namestore next to a registered zone key. 64 // It is always stored in a private metadata record and holds registration information such 65 // as payment status and expiration. 66 type RegistrationMetadata struct { 67 // The expiration in GNS-compatible 64-bit microseconds epoch. 68 Expiration uint64 `json:"expiration"` 69 70 // Indication if this registration is already paid and active. 71 Paid bool `json:"paid"` 72 73 // The unique order identifier (as received through https://docs.taler.net/core/api-merchant.html). 74 OrderID string `json:"order_id"` 75 76 // The unique registration identifier used as token for registration management by customer. 77 RegistrationID string `json:"registration_id"` 78 79 // The payment deadline. FIXME this may also be part of the order somehow in the Taler API. 80 NeedsPaymentUntil time.Time `json:"needs_payment_until"` 81 } 82 83 // See https://docs.gnunet.org/latest/developers/rest-api/identity.html 84 type IdentityInfo struct { 85 // Base32-encoded GNS zone key 86 Pubkey string `json:"pubkey"` 87 88 // The name of the zone/identity 89 Name string `json:"name"` 90 } 91 92 // See https://gana.gnunet.org/gnunet-error-codes/gnunet_error_codes.html 93 type GnunetError struct { 94 // Error description 95 Description string `json:"error"` 96 97 // Error code 98 Code uint32 `json:"error_code"` 99 } 100 101 // See https://docs.gnunet.org/latest/developers/rest-api/namestore.html 102 type RecordData struct { 103 // The string representation of the record data value, e.g. "1.2.3.4" for an A record 104 Value string `json:"value"` 105 106 // The string representation of the record type, e.g. "A" for an IPv4 address 107 RecordType string `json:"record_type"` 108 109 // The relative expiration time, in microseconds. Set if is_relative_expiration: true 110 RelativeExpiration uint64 `json:"relative_expiration"` 111 112 // Whether or not this is a private record 113 IsPrivate bool `json:"is_private"` 114 115 // Whether or not the expiration time is relative (else absolute) 116 IsRelativeExpiration bool `json:"is_relative_expiration"` 117 118 // Whether or not this is a supplemental record 119 IsSupplemental bool `json:"is_supplemental"` 120 121 // Whether or not this is a shadow record 122 IsShadow bool `json:"is_shadow"` 123 124 // Whether or not this is a maintenance record 125 IsMaintenance bool `json:"is_maintenance"` 126 } 127 128 // See https://docs.gnunet.org/latest/developers/rest-api/namestore.html 129 type NamestoreRecord struct { 130 // Name of the record set 131 RecordName string `json:"record_name"` 132 133 // The record set 134 Records []RecordData `json:"data"` 135 } 136 137 // Registrar is the primary object of the service 138 type Registrar struct { 139 140 // The main router 141 Router *mux.Router 142 143 // Our configuration from the config.json 144 Cfg RegistrarConfig 145 146 // Logger 147 Logger *log.Logger 148 149 // Map of supported validators as defined in the configuration 150 Validators map[string]bool 151 152 // landing page 153 LandingTpl *template.Template 154 155 // name page 156 NameTpl *template.Template 157 158 // buy names page 159 BuyTpl *template.Template 160 161 // edit registration page 162 EditTpl *template.Template 163 164 // Merchant object 165 Merchant merchant.Merchant 166 167 // Relative record expiration (NOT registration expiration!) 168 RelativeDelegationExpiration time.Duration 169 170 // Registration expiration (NOT record expiration!) 171 RelativeRegistrationExpiration time.Duration 172 173 // Registration expiration days count 174 RegistrationExpirationDaysCount uint64 175 176 // Payment expiration (time you have to pay for registration) 177 PaymentExpiration time.Duration 178 179 // Name of our root zone 180 RootZoneName string 181 182 // Key of our root zone 183 RootZoneKey string 184 185 // Suggested suffix for our zone 186 SuffixHint string 187 188 // Gnunet REST API basename 189 GnunetUrl string 190 191 // Gnunet basic auth on/off 192 GnunetBasicAuthEnabled bool 193 194 // Gnunet basic auth 195 GnunetUsername string 196 197 // Gnunet basic auth 198 GnunetPassword string 199 200 // Registrar base URL 201 BaseUrl string 202 203 // The template to use for the summary string 204 SummaryTemplateString string 205 206 // Valid label regex 207 ValidLabelRegex string 208 209 // Valid label script 210 ValidLabelScript string 211 212 // Cost for a registration 213 RegistrationCost *talerutil.Amount 214 215 // Cost for a registration 216 CurrencySpec talerutil.CurrencySpecification 217 } 218 219 type VersionResponse struct { 220 // libtool-style representation of the Merchant protocol version, see 221 // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning 222 // The format is "current:revision:age". 223 Version string `json:"version"` 224 } 225 226 func (t *Registrar) configResponse(w http.ResponseWriter, r *http.Request) { 227 cfg := VersionResponse{ 228 Version: "0:0:0", 229 } 230 w.Header().Set("Content-Type", "application/json") 231 response, err := json.Marshal(cfg) 232 if nil != err { 233 t.Logf(LogError, "%s\n", err.Error()) 234 return 235 } 236 w.Write(response) 237 } 238 239 func generateRegistrationId() string { 240 return uuid.New().String() 241 } 242 243 func (t *Registrar) landingPage(w http.ResponseWriter, r *http.Request) { 244 w.Header().Set("Content-Type", "text/html; charset=utf-8") 245 246 fullData := map[string]any{ 247 "suffixHint": t.SuffixHint, 248 "extZkey": r.URL.Query().Get("zkey"), 249 "zoneKey": t.RootZoneKey, 250 "version": t.Cfg.Version, 251 "error": r.URL.Query().Get("error"), 252 } 253 err := t.LandingTpl.Execute(w, fullData) 254 if err != nil { 255 t.Logf(LogError, "%s\n", err.Error()) 256 } 257 } 258 259 func (t *Registrar) isNameValid(label string) (err error) { 260 if label == "@" { 261 return fmt.Errorf("'%s' invalid: '@' not allowed", label) 262 } 263 if strings.Contains(label, ".") { 264 return fmt.Errorf("'%s' invalid: '.' not allowed", label) 265 } 266 if t.ValidLabelRegex != "" { 267 matched, _ := regexp.MatchString(t.ValidLabelRegex, label) 268 if !matched { 269 return fmt.Errorf("label '%s' not allowed by policy", label) 270 } 271 } 272 if t.ValidLabelScript != "" { 273 path, err := exec.LookPath(t.ValidLabelScript) 274 if err != nil { 275 t.Logf(LogError, "%s\n", err.Error()) 276 return errors.New("internal error") 277 } 278 _, err = exec.Command(path, label).Output() 279 if err != nil { 280 return fmt.Errorf("label '%s' not allowed by policy", label) 281 } 282 } 283 return 284 } 285 286 func (t *Registrar) searchPage(w http.ResponseWriter, r *http.Request) { 287 var ( 288 label string 289 zkey string 290 err error 291 ) 292 w.Header().Set("Content-Type", "text/html; charset=utf-8") 293 label = r.URL.Query().Get("label") 294 err = t.isNameValid(label) 295 if nil != err { 296 http.Redirect(w, r, fmt.Sprintf("/?error=%s", err), http.StatusSeeOther) 297 return 298 } 299 zkey = r.URL.Query().Get("zkey") 300 if zkey == "" { 301 http.Redirect(w, r, "/name/"+url.QueryEscape(label), http.StatusSeeOther) 302 } else { 303 http.Redirect(w, r, "/name/"+url.QueryEscape(label)+"?zkey="+url.QueryEscape(zkey), http.StatusSeeOther) 304 } 305 } 306 307 func (t *Registrar) expireRegistration(label string) (err error) { 308 var ( 309 gnunetError GnunetError 310 client *http.Client 311 ) 312 client = &http.Client{} 313 req, _ := http.NewRequest(http.MethodDelete, t.GnunetUrl+"/namestore/"+t.RootZoneName+"/"+label, nil) 314 if t.GnunetBasicAuthEnabled { 315 req.SetBasicAuth(t.GnunetUsername, t.GnunetPassword) 316 } 317 resp, err := client.Do(req) 318 if err != nil { 319 return err 320 } 321 err = resp.Body.Close() 322 if err != nil { 323 return err 324 } 325 if http.StatusNotFound == resp.StatusCode { 326 return nil 327 } 328 if http.StatusNoContent != resp.StatusCode { 329 t.Logf(LogError, "Got error: %d\n", resp.StatusCode) 330 _ = json.NewDecoder(resp.Body).Decode(&gnunetError) 331 return errors.New("GNUnet REST API error: " + gnunetError.Description) 332 } 333 return nil 334 } 335 336 func (t *Registrar) createOrUpdateRegistration(nsRecord *NamestoreRecord) (err error) { 337 var gnunetError GnunetError 338 reqString, _ := json.Marshal(nsRecord) 339 client := &http.Client{} 340 req, _ := http.NewRequest(http.MethodPut, t.GnunetUrl+"/namestore/"+t.RootZoneName, bytes.NewBuffer(reqString)) 341 if t.GnunetBasicAuthEnabled { 342 req.SetBasicAuth(t.GnunetUsername, t.GnunetPassword) 343 } 344 resp, err := client.Do(req) 345 if nil != err { 346 return err 347 } 348 err = resp.Body.Close() 349 if err != nil { 350 return err 351 } 352 if http.StatusNoContent != resp.StatusCode { 353 t.Logf(LogError, "Got error: %d\n", resp.StatusCode) 354 err = json.NewDecoder(resp.Body).Decode(&gnunetError) 355 if nil != err { 356 return errors.New("GNUnet REST API error: " + err.Error()) 357 } 358 return errors.New("GNUnet REST API error: " + gnunetError.Description) 359 } 360 return nil 361 } 362 363 func getEndOfDay(day time.Time) time.Time { 364 return time.Date(day.Year(), day.Month(), day.Day(), 23, 59, 59, 0, day.Location()) 365 } 366 367 func (t *Registrar) setupRegistrationMetadataBeforePayment(label string, zkey string, orderId string, paymentUntil time.Time, regId string) (err error) { 368 var ( 369 namestoreRequest NamestoreRecord 370 delegationRecord RecordData 371 metadataRecord RecordData 372 registrationMetadata RegistrationMetadata 373 ) 374 delegationRecord.IsPrivate = true // Private until payment is through 375 delegationRecord.IsRelativeExpiration = true 376 delegationRecord.IsSupplemental = false 377 delegationRecord.IsMaintenance = false 378 delegationRecord.IsShadow = false 379 delegationRecord.RecordType = guessDelegationRecordType(zkey) 380 delegationRecord.RelativeExpiration = uint64(t.RelativeDelegationExpiration.Microseconds()) 381 delegationRecord.Value = zkey 382 metadataRecord.IsPrivate = true 383 metadataRecord.IsRelativeExpiration = true 384 metadataRecord.IsSupplemental = false 385 metadataRecord.IsMaintenance = true 386 metadataRecord.IsShadow = false 387 metadataRecord.RecordType = "TXT" // FIXME use new recory type 388 metadataRecord.RelativeExpiration = uint64(t.RelativeDelegationExpiration.Microseconds()) 389 registrationMetadata = RegistrationMetadata{ 390 Paid: false, 391 OrderID: orderId, 392 NeedsPaymentUntil: paymentUntil, 393 RegistrationID: regId, 394 Expiration: uint64(getEndOfDay(time.Now()).UnixMicro()), 395 } 396 metadataRecordValue, err := json.Marshal(registrationMetadata) 397 if nil != err { 398 return err 399 } 400 metadataRecord.Value = string(metadataRecordValue) 401 namestoreRequest.RecordName = label 402 namestoreRequest.Records = []RecordData{delegationRecord, metadataRecord} 403 return t.createOrUpdateRegistration(&namestoreRequest) 404 } 405 406 func (t *Registrar) updateRegistration(w http.ResponseWriter, r *http.Request) { 407 var ( 408 namestoreResponse NamestoreRecord 409 zkeyRecord RecordData 410 metaRecord RecordData 411 regMetadata *RegistrationMetadata 412 client *http.Client 413 token string 414 zkey string 415 ) 416 vars := mux.Vars(r) 417 sanitizedLabel := url.QueryEscape(vars["label"]) 418 w.Header().Set("Content-Type", "text/html; charset=utf-8") 419 client = &http.Client{} 420 req, _ := http.NewRequest(http.MethodGet, t.GnunetUrl+"/namestore/"+t.RootZoneName+"/"+sanitizedLabel+"?include_maintenance=yes", nil) 421 if t.GnunetBasicAuthEnabled { 422 req.SetBasicAuth(t.GnunetUsername, t.GnunetPassword) 423 } 424 resp, err := client.Do(req) 425 if err != nil { 426 http.Redirect(w, r, "/"+"?error=Registration failed: Failed to get zone contents", http.StatusSeeOther) 427 t.Logf(LogError, "Failed to get zone contents\n") 428 return 429 } 430 defer resp.Body.Close() 431 if http.StatusOK == resp.StatusCode { 432 respData, err := io.ReadAll(resp.Body) 433 if err != nil { 434 http.Redirect(w, r, "/"+"?error=Registration failed: Failed to get zone contents", http.StatusSeeOther) 435 t.Logf(LogError, "Failed to get zone contents: `%s'\n", err.Error()) 436 return 437 } 438 err = json.NewDecoder(bytes.NewReader(respData)).Decode(&namestoreResponse) 439 if err != nil { 440 http.Redirect(w, r, "/"+"?error=Registration failed: Failed to get zone contents", http.StatusSeeOther) 441 t.Logf(LogError, "Failed to get zone contents: `%s'\n", err.Error()) 442 return 443 } 444 regMetadata, err = t.getCurrentRegistrationMetadata(vars["label"], &namestoreResponse) 445 if err != nil { 446 http.Redirect(w, r, "/"+"?error=Failed to get registration metadata", http.StatusSeeOther) 447 t.Logf(LogError, "Failed to get registration metadata: `%s'\n", err.Error()) 448 return 449 } 450 } else if http.StatusNotFound != resp.StatusCode { 451 http.Redirect(w, r, "/name/"+sanitizedLabel+"?error=Error determining zone status", http.StatusSeeOther) 452 return 453 } 454 if nil == regMetadata { 455 http.Redirect(w, r, "/name/"+sanitizedLabel, http.StatusSeeOther) 456 return 457 } 458 err = r.ParseForm() 459 if nil != err { 460 t.Logf(LogError, "Unable to parse form: `%s'\n", err.Error()) 461 http.Redirect(w, r, "/name/"+sanitizedLabel+"?error=Form invalid", http.StatusSeeOther) 462 return 463 } 464 token = r.Form.Get("token") 465 zkey = r.Form.Get("zkey") 466 if regMetadata.RegistrationID != token { 467 http.Redirect(w, r, "/name/"+sanitizedLabel+"?error=Unauthorized", http.StatusSeeOther) 468 return 469 } 470 for _, record := range namestoreResponse.Records { 471 if isDelegationRecordType(record.RecordType) { 472 zkeyRecord = record 473 } else { 474 metaRecord = record 475 } 476 } 477 zkeyRecord.Value = zkey 478 zkeyRecord.RecordType = guessDelegationRecordType(zkey) 479 namestoreResponse.Records = []RecordData{metaRecord, zkeyRecord} 480 err = t.createOrUpdateRegistration(&namestoreResponse) 481 if nil != err { 482 t.Logf(LogError, "%s\n", err.Error()) 483 http.Redirect(w, r, "/name/"+sanitizedLabel+"?error=Update: Internal error", http.StatusSeeOther) 484 return 485 } 486 http.Redirect(w, r, "/name/"+sanitizedLabel+"/edit?token="+token, http.StatusSeeOther) 487 } 488 489 func (t *Registrar) editRegistration(w http.ResponseWriter, r *http.Request) { 490 var ( 491 namestoreResponse NamestoreRecord 492 regMetadata *RegistrationMetadata 493 value string 494 client *http.Client 495 ) 496 vars := mux.Vars(r) 497 sanitizedLabel := url.QueryEscape(vars["label"]) 498 w.Header().Set("Content-Type", "text/html; charset=utf-8") 499 client = &http.Client{} 500 req, _ := http.NewRequest(http.MethodGet, t.GnunetUrl+"/namestore/"+t.RootZoneName+"/"+sanitizedLabel+"?include_maintenance=yes", nil) 501 if t.GnunetBasicAuthEnabled { 502 req.SetBasicAuth(t.GnunetUsername, t.GnunetPassword) 503 } 504 resp, err := client.Do(req) 505 if err != nil { 506 http.Redirect(w, r, "/"+"?error=Registration failed: Failed to get zone contents", http.StatusSeeOther) 507 t.Logf(LogError, "Failed to get zone contents\n") 508 return 509 } 510 defer resp.Body.Close() 511 if http.StatusOK == resp.StatusCode { 512 respData, err := io.ReadAll(resp.Body) 513 if err != nil { 514 http.Redirect(w, r, "/"+"?error=Registration failed: Failed to get zone contents", http.StatusSeeOther) 515 t.Logf(LogError, "Failed to get zone contents: `%s'\n", err.Error()) 516 return 517 } 518 err = json.NewDecoder(bytes.NewReader(respData)).Decode(&namestoreResponse) 519 if err != nil { 520 http.Redirect(w, r, "/"+"?error=Registration failed: Failed to get zone contents", http.StatusSeeOther) 521 t.Logf(LogError, "Failed to get zone contents: `%s'\n", err.Error()) 522 return 523 } 524 regMetadata, err = t.getCurrentRegistrationMetadata(vars["label"], &namestoreResponse) 525 if err != nil { 526 http.Redirect(w, r, "/"+"?error=Failed to get registration metadata", http.StatusSeeOther) 527 t.Logf(LogError, "Failed to get registration metadata: `%s'\n", err.Error()) 528 return 529 } 530 } else if http.StatusNotFound != resp.StatusCode { 531 http.Redirect(w, r, "/name/"+sanitizedLabel+"?error=Error determining zone status", http.StatusSeeOther) 532 return 533 } 534 if nil == regMetadata { 535 http.Redirect(w, r, "/name/"+sanitizedLabel, http.StatusSeeOther) 536 return 537 } 538 if regMetadata.RegistrationID != r.URL.Query().Get("token") { 539 http.Redirect(w, r, "/name/"+sanitizedLabel+"?error=Unauthorized", http.StatusSeeOther) 540 return 541 } 542 if !regMetadata.Paid { 543 http.Redirect(w, r, "/name/"+sanitizedLabel+"/buy/payment?token="+regMetadata.RegistrationID, http.StatusSeeOther) 544 return 545 } 546 for _, record := range namestoreResponse.Records { 547 if isDelegationRecordType(record.RecordType) { 548 value = record.Value 549 } 550 } 551 registeredUntil := time.UnixMicro(int64(regMetadata.Expiration)) 552 registeredUntilStr := registeredUntil.Format(time.DateTime) 553 remainingDays := int64(time.Until(registeredUntil).Hours() / 24) 554 extendedExpiration := time.UnixMicro(int64(regMetadata.Expiration)).Add(t.RelativeRegistrationExpiration).Format(time.DateTime) 555 cost, _ := t.RegistrationCost.FormatWithCurrencySpecification(t.CurrencySpec) 556 fullData := map[string]any{ 557 "label": vars["label"], 558 "zkey": value, 559 "extendedExpiration": extendedExpiration, 560 "extensionDaysCount": t.RegistrationExpirationDaysCount, 561 "remainingDays": remainingDays, 562 "registeredUntil": registeredUntilStr, 563 "token": r.URL.Query().Get("token"), 564 "version": t.Cfg.Version, 565 "error": r.URL.Query().Get("error"), 566 "cost": cost, 567 "suffixHint": t.SuffixHint, 568 } 569 err = t.EditTpl.Execute(w, fullData) 570 if err != nil { 571 t.Logf(LogError, "%s\n", err.Error()) 572 } 573 } 574 575 func (t *Registrar) paymentPage(w http.ResponseWriter, r *http.Request) { 576 var ( 577 namestoreResponse NamestoreRecord 578 regMetadata *RegistrationMetadata 579 client *http.Client 580 label string 581 errorMsg string 582 ) 583 vars := mux.Vars(r) 584 w.Header().Set("Content-Type", "text/html; charset=utf-8") 585 client = &http.Client{} 586 label = vars["label"] 587 sanitizedLabel := url.QueryEscape(vars["label"]) 588 err := t.isNameValid(label) 589 if nil != err { 590 http.Redirect(w, r, fmt.Sprintf("/?error=%s", err), http.StatusSeeOther) 591 return 592 } 593 req, _ := http.NewRequest(http.MethodGet, t.GnunetUrl+"/namestore/"+t.RootZoneName+"/"+sanitizedLabel+"?include_maintenance=yes", nil) 594 if t.GnunetBasicAuthEnabled { 595 req.SetBasicAuth(t.GnunetUsername, t.GnunetPassword) 596 } 597 resp, err := client.Do(req) 598 if err != nil { 599 http.Redirect(w, r, "/"+"?error=Registration failed: Failed to get zone contents", http.StatusSeeOther) 600 t.Logf(LogError, "Failed to get zone contents\n") 601 return 602 } 603 defer resp.Body.Close() 604 if http.StatusOK == resp.StatusCode { 605 respData, err := io.ReadAll(resp.Body) 606 if err != nil { 607 http.Redirect(w, r, "/"+"?error=Registration failed: Failed to get zone contents", http.StatusSeeOther) 608 t.Logf(LogError, "Failed to get zone contents: `%s'\n", err.Error()) 609 return 610 } 611 err = json.NewDecoder(bytes.NewReader(respData)).Decode(&namestoreResponse) 612 if err != nil { 613 http.Redirect(w, r, "/"+"?error=Registration failed: Failed to get zone contents", http.StatusSeeOther) 614 t.Logf(LogError, "Failed to get zone contents: `%s'\n", err.Error()) 615 return 616 } 617 regMetadata, err = t.getCurrentRegistrationMetadata(sanitizedLabel, &namestoreResponse) 618 if err != nil { 619 http.Redirect(w, r, "/"+"?error=Registration failed: Failed to get registration metadata", http.StatusSeeOther) 620 t.Logf(LogError, "Failed to get registration metadata: `%s'\n", err.Error()) 621 return 622 } 623 } else if http.StatusNotFound != resp.StatusCode { 624 http.Redirect(w, r, "/name/"+sanitizedLabel+"?error=Payment failed: Error determining zone status", http.StatusSeeOther) 625 return 626 } 627 if nil == regMetadata { 628 http.Redirect(w, r, "/name/"+sanitizedLabel+"?error=Payment failed: Please try again.", http.StatusSeeOther) 629 return 630 } 631 if regMetadata.Paid { 632 http.Redirect(w, r, "/name/"+sanitizedLabel+"?error=Payment already paid.", http.StatusSeeOther) 633 return 634 } 635 _, orderStatus, payto, paytoErr := t.Merchant.IsOrderPaid(regMetadata.OrderID) 636 if paytoErr != nil { 637 http.Redirect(w, r, "/name/"+sanitizedLabel+"?error=Payment failed: Error getting payment data", http.StatusSeeOther) 638 return 639 } 640 encodedPng := "" 641 if payto != "" { 642 qrPng, qrErr := qrcode.Encode(payto, qrcode.Medium, 256) 643 if qrErr != nil { 644 http.Redirect(w, r, "/name/"+sanitizedLabel+"?error=Registration failed: Error generating QR code", http.StatusSeeOther) 645 return 646 } 647 encodedPng = base64.StdEncoding.EncodeToString(qrPng) 648 } 649 cost, _ := t.RegistrationCost.FormatWithCurrencySpecification(t.CurrencySpec) 650 w.Header().Set("Refresh", "20;url="+t.BaseUrl+"/name/"+sanitizedLabel+"/edit?token="+regMetadata.RegistrationID) 651 fullData := map[string]interface{}{ 652 "orderUnpaid": merchant.OrderUnpaid == orderStatus, 653 "qrCode": template.URL("data:image/png;base64," + encodedPng), 654 "payto": template.URL(payto), 655 "fulfillmentUrl": template.URL(t.BaseUrl + "/name/" + sanitizedLabel + "/edit?token=" + regMetadata.RegistrationID), 656 "registrationId": regMetadata.RegistrationID, 657 "label": sanitizedLabel, 658 "version": t.Cfg.Version, 659 "error": errorMsg, 660 "cost": cost, 661 "suffixHint": t.SuffixHint, 662 } 663 err = t.BuyTpl.Execute(w, fullData) 664 if err != nil { 665 t.Logf(LogError, "%s\n", err.Error()) 666 } 667 } 668 669 func (t *Registrar) buyPage(w http.ResponseWriter, r *http.Request) { 670 var ( 671 namestoreResponse NamestoreRecord 672 regMetadata *RegistrationMetadata 673 client *http.Client 674 label string 675 regId string 676 ) 677 vars := mux.Vars(r) 678 w.Header().Set("Content-Type", "text/html; charset=utf-8") 679 client = &http.Client{} 680 label = vars["label"] 681 sanitizedLabel := url.QueryEscape(vars["label"]) 682 err := t.isNameValid(label) 683 if nil != err { 684 http.Redirect(w, r, fmt.Sprintf("/?error=%s", err), http.StatusSeeOther) 685 return 686 } 687 req, _ := http.NewRequest(http.MethodGet, t.GnunetUrl+"/namestore/"+t.RootZoneName+"/"+sanitizedLabel+"?include_maintenance=yes", nil) 688 if t.GnunetBasicAuthEnabled { 689 req.SetBasicAuth(t.GnunetUsername, t.GnunetPassword) 690 } 691 resp, err := client.Do(req) 692 if err != nil { 693 http.Redirect(w, r, "/"+"?error=Registration failed: Failed to get zone contents", http.StatusSeeOther) 694 t.Logf(LogError, "Failed to get zone contents\n") 695 return 696 } 697 defer resp.Body.Close() 698 if http.StatusOK == resp.StatusCode { 699 respData, err := io.ReadAll(resp.Body) 700 if err != nil { 701 http.Redirect(w, r, "/"+"?error=Registration failed: Failed to get zone contents", http.StatusSeeOther) 702 t.Logf(LogError, "Failed to get zone contents: `%s'\n", err.Error()) 703 return 704 } 705 err = json.NewDecoder(bytes.NewReader(respData)).Decode(&namestoreResponse) 706 if err != nil { 707 http.Redirect(w, r, "/"+"?error=Registration failed: Failed to get zone contents", http.StatusSeeOther) 708 t.Logf(LogError, "Failed to get zone contents: `%s'\n", err.Error()) 709 return 710 } 711 regMetadata, err = t.getCurrentRegistrationMetadata(sanitizedLabel, &namestoreResponse) 712 if err != nil { 713 http.Redirect(w, r, "/"+"?error=Registration failed: Failed to get registration metadata", http.StatusSeeOther) 714 t.Logf(LogError, "Failed to get registration metadata: `%s'\n", err.Error()) 715 return 716 } 717 } else if http.StatusNotFound != resp.StatusCode { 718 http.Redirect(w, r, "/name/"+sanitizedLabel+"?error=Registration failed: Error determining zone status", http.StatusSeeOther) 719 return 720 } 721 if nil != regMetadata { 722 if !regMetadata.Paid { 723 http.Redirect(w, r, "/name/"+sanitizedLabel+"?error=Registration failed: Pending buy order", http.StatusSeeOther) 724 return 725 } 726 regMetadata.Paid = false 727 regId = regMetadata.RegistrationID 728 } else { 729 regId = generateRegistrationId() 730 } 731 summaryMsg := strings.Replace(t.SummaryTemplateString, "${NAME}", label, 1) 732 orderID, newOrderErr := t.Merchant.AddNewOrder(*t.RegistrationCost, summaryMsg, t.BaseUrl+"/name/"+sanitizedLabel+"/edit?token="+regId) 733 if newOrderErr != nil { 734 t.Logf(LogError, "%s\n", newOrderErr.Error()) 735 http.Redirect(w, r, "/name/"+sanitizedLabel+"?error=Registration failed: Unable to create order", http.StatusSeeOther) 736 return 737 } 738 // FIXME: based on order status, we probably want to display something else 739 _, _, _, paytoErr := t.Merchant.IsOrderPaid(orderID) 740 if paytoErr != nil { 741 http.Redirect(w, r, "/name/"+sanitizedLabel+"?error=Registration failed: Error getting payment data", http.StatusSeeOther) 742 return 743 } 744 paymentUntil := time.Now().Add(t.PaymentExpiration) 745 if nil != regMetadata { 746 var newZkeyRecord RecordData 747 var metaRecord RecordData 748 for _, record := range namestoreResponse.Records { 749 if isDelegationRecordType(record.RecordType) { 750 record.IsPrivate = false 751 newZkeyRecord = record 752 } else { 753 metaRecord = record 754 } 755 } 756 regMetadata.NeedsPaymentUntil = paymentUntil 757 regMetadata.OrderID = orderID 758 metadataRecordValue, err := json.Marshal(regMetadata) 759 if nil != err { 760 t.Logf(LogError, "%s\n", err.Error()) 761 http.Redirect(w, r, "/name/"+sanitizedLabel+"?error=Registration failed: Internal error", http.StatusSeeOther) 762 return 763 } 764 metaRecord.Value = string(metadataRecordValue) 765 namestoreResponse.Records = []RecordData{metaRecord, newZkeyRecord} 766 err = t.createOrUpdateRegistration(&namestoreResponse) 767 if nil != err { 768 t.Logf(LogError, "%s\n", err.Error()) 769 http.Redirect(w, r, "/name/"+sanitizedLabel+"?error=Registration failed: Internal error", http.StatusSeeOther) 770 return 771 } 772 } else { 773 err = t.setupRegistrationMetadataBeforePayment(sanitizedLabel, r.URL.Query().Get("zkey"), orderID, paymentUntil, regId) 774 if err != nil { 775 t.Logf(LogError, "%s\n", err.Error()) 776 http.Redirect(w, r, "/name/"+sanitizedLabel+"?error=Registration failed: Internal error", http.StatusSeeOther) 777 return 778 } 779 } 780 http.Redirect(w, r, "/name/"+sanitizedLabel+"/buy/payment?token="+regId, http.StatusSeeOther) 781 } 782 783 func (t *Registrar) getCurrentRegistrationMetadata(label string, nsRecord *NamestoreRecord) (*RegistrationMetadata, error) { 784 var ( 785 regMetadata RegistrationMetadata 786 haveMetadata bool 787 ) 788 haveMetadata = false 789 for _, record := range nsRecord.Records { 790 if record.RecordType == "TXT" { 791 err := json.Unmarshal([]byte(record.Value), ®Metadata) 792 if err != nil { 793 t.Logf(LogError, "Failed to get zone contents: `%s'\n", err.Error()) 794 return nil, err 795 } 796 haveMetadata = true 797 } 798 } 799 if !haveMetadata { 800 return nil, nil 801 } 802 // Does this registration have an unpaid order? if yes, check payment status and update if necessary. 803 if !regMetadata.Paid { 804 rc, orderStatus, _, paytoErr := t.Merchant.IsOrderPaid(regMetadata.OrderID) 805 if nil != paytoErr { 806 if rc == http.StatusNotFound { 807 if time.Now().After(time.UnixMicro(int64(regMetadata.Expiration))) { 808 t.Logf(LogInfo, "Registration for `%s' not found, removing\n", label) 809 err := t.expireRegistration(label) 810 if nil != err { 811 t.Logf(LogInfo, "%s\n", err.Error()) 812 } 813 return nil, nil 814 } else { 815 return ®Metadata, nil 816 } 817 } 818 return nil, errors.New("Error determining payment status: " + paytoErr.Error()) 819 } 820 if merchant.OrderPaid == orderStatus { 821 // Order was paid! 822 regMetadata.Paid = true 823 var newZkeyRecord RecordData 824 var newMetaRecord RecordData 825 for _, record := range nsRecord.Records { 826 if isDelegationRecordType(record.RecordType) { 827 record.IsPrivate = false 828 newZkeyRecord = record 829 } 830 if record.RecordType == "TXT" { 831 metadataRecordValue, err := json.Marshal(regMetadata) 832 if nil != err { 833 return nil, err 834 } 835 record.Value = string(metadataRecordValue) 836 newMetaRecord = record 837 } 838 } 839 // Note how for every time the payment is completed, the registration duration increases 840 regMetadata.Expiration += uint64(t.RelativeRegistrationExpiration.Microseconds()) 841 metadataRecordValue, err := json.Marshal(regMetadata) 842 if nil != err { 843 return nil, err 844 } 845 newMetaRecord.Value = string(metadataRecordValue) 846 nsRecord.Records = []RecordData{newMetaRecord, newZkeyRecord} 847 err = t.createOrUpdateRegistration(nsRecord) 848 if nil != err { 849 return nil, err 850 } 851 return ®Metadata, nil 852 } else { 853 // Remove metadata if payment limit exceeded and registration expired 854 if time.Now().After(regMetadata.NeedsPaymentUntil) && time.Now().After(time.UnixMicro(int64(regMetadata.Expiration))) { 855 t.Logf(LogDebug, "Payment request for `%s' has expired, removing\n", label) 856 err := t.expireRegistration(label) 857 if nil != err { 858 t.Logf(LogInfo, "%s\n", err.Error()) 859 } 860 return nil, nil 861 } 862 } 863 } else { 864 if time.Now().After(time.UnixMicro(int64(regMetadata.Expiration))) { 865 t.Logf(LogDebug, "Registration for `%s' has expired, removing\n", label) 866 err := t.expireRegistration(label) 867 if nil != err { 868 t.Logf(LogInfo, "%s\n", err.Error()) 869 } 870 return nil, nil 871 } 872 } 873 return ®Metadata, nil 874 } 875 876 func guessDelegationRecordType(val string) string { 877 if strings.HasPrefix(val, "000G00") { 878 return "PKEY" 879 } else { 880 return "EDKEY" 881 } 882 } 883 884 func isDelegationRecordType(typ string) bool { 885 return typ == "PKEY" || typ == "EDKEY" 886 } 887 888 func (t *Registrar) namePage(w http.ResponseWriter, r *http.Request) { 889 var ( 890 namestoreResponse NamestoreRecord 891 value string 892 registeredUntilStr string 893 regMetadata *RegistrationMetadata 894 remainingDays int64 895 client *http.Client 896 registered = r.URL.Query().Get("registered") == "true" 897 ) 898 vars := mux.Vars(r) 899 w.Header().Set("Content-Type", "text/html; charset=utf-8") 900 label := vars["label"] 901 err := t.isNameValid(label) 902 sanitizedLabel := url.QueryEscape(vars["label"]) 903 if nil != err { 904 http.Redirect(w, r, fmt.Sprintf("/?error=%s", err), http.StatusSeeOther) 905 return 906 } 907 client = &http.Client{} 908 req, _ := http.NewRequest(http.MethodGet, t.GnunetUrl+"/namestore/"+t.RootZoneName+"/"+sanitizedLabel+"?include_maintenance=yes", nil) 909 if t.GnunetBasicAuthEnabled { 910 req.SetBasicAuth(t.GnunetUsername, t.GnunetPassword) 911 } 912 resp, err := client.Do(req) 913 if err != nil { 914 http.Redirect(w, r, "/"+"?error=Failed to get zone contents.", http.StatusSeeOther) 915 t.Logf(LogError, "Failed to get zone contents\n") 916 return 917 } 918 defer resp.Body.Close() 919 if http.StatusOK == resp.StatusCode { 920 respData, err := io.ReadAll(resp.Body) 921 if err != nil { 922 t.Logf(LogError, "Failed to get zone contents: `%s'\n", err.Error()) 923 http.Redirect(w, r, "/"+"?error=Failed to get zone contents.", http.StatusSeeOther) 924 return 925 } 926 err = json.NewDecoder(bytes.NewReader(respData)).Decode(&namestoreResponse) 927 if err != nil { 928 t.Logf(LogError, "Failed to get zone contents: `%s'\n", err.Error()) 929 http.Redirect(w, r, "/"+"?error=Failed to get zone contents.", http.StatusSeeOther) 930 return 931 } 932 regMetadata, err = t.getCurrentRegistrationMetadata(sanitizedLabel, &namestoreResponse) 933 if err != nil { 934 t.Logf(LogError, "Failed to get registration metadata: `%s'\n", err.Error()) 935 http.Redirect(w, r, "/"+"?error=Failed to get registration metadata.", http.StatusSeeOther) 936 return 937 } 938 } else if http.StatusNotFound != resp.StatusCode { 939 http.Redirect(w, r, "/"+"?error=Error retrieving zone information.", http.StatusSeeOther) 940 return 941 } 942 for _, record := range namestoreResponse.Records { 943 if isDelegationRecordType(record.RecordType) { 944 value = record.Value 945 } 946 } 947 if regMetadata != nil { 948 if time.Now().Before(time.UnixMicro(int64(regMetadata.Expiration))) { 949 registeredUntil := time.UnixMicro(int64(regMetadata.Expiration)) 950 registeredUntilStr = registeredUntil.Format(time.DateTime) 951 remainingDays = int64(time.Until(registeredUntil).Hours() / 24) 952 } 953 } 954 cost, _ := t.RegistrationCost.FormatWithCurrencySpecification(t.CurrencySpec) 955 fullData := map[string]any{ 956 "label": sanitizedLabel, 957 "version": t.Cfg.Version, 958 "error": r.URL.Query().Get("error"), 959 "zkey": r.URL.Query().Get("zkey"), 960 "cost": cost, 961 "available": regMetadata == nil, 962 "currentValue": value, 963 "suffixHint": t.SuffixHint, 964 "registrationDaysCount": t.RegistrationExpirationDaysCount, 965 "registeredUntil": registeredUntilStr, 966 "remainingDays": remainingDays, 967 "registrationSuccess": registered, 968 } 969 err = t.NameTpl.Execute(w, fullData) 970 if err != nil { 971 t.Logf(LogError, "%s\n", err.Error()) 972 } 973 } 974 975 type LogLevel int 976 977 const ( 978 LogError LogLevel = iota 979 LogWarning 980 LogInfo 981 LogDebug 982 ) 983 984 var LoglevelStringMap = map[LogLevel]string{ 985 LogDebug: "DEBUG", 986 LogError: "ERROR", 987 LogWarning: "WARN", 988 LogInfo: "INFO", 989 } 990 991 func (t *Registrar) Logf(loglevel LogLevel, fmt string, args ...any) { 992 if loglevel > t.Cfg.Loglevel { 993 return 994 } 995 t.Logger.SetPrefix("taler-directory - " + LoglevelStringMap[loglevel] + " ") 996 t.Logger.Printf(fmt, args...) 997 } 998 999 func (t *Registrar) getFileName(relativeFileName string) string { 1000 _, err := os.Stat(relativeFileName) 1001 if errors.Is(err, os.ErrNotExist) { 1002 _, err := os.Stat(t.Cfg.Datahome + "/" + relativeFileName) 1003 if errors.Is(err, os.ErrNotExist) { 1004 t.Logf(LogError, "Tried fallback not found `%s'\n", t.Cfg.Datahome+"/"+relativeFileName) 1005 return "" 1006 } 1007 return t.Cfg.Datahome + "/" + relativeFileName 1008 } 1009 return relativeFileName 1010 } 1011 1012 func (t *Registrar) setupHandlers() { 1013 t.Router = mux.NewRouter().StrictSlash(true) 1014 1015 t.Router.HandleFunc("/", t.landingPage).Methods("GET") 1016 t.Router.HandleFunc("/name/{label}", t.namePage).Methods("GET") 1017 t.Router.HandleFunc("/name/{label}/buy", t.buyPage).Methods("GET") 1018 t.Router.HandleFunc("/name/{label}/buy/payment", t.paymentPage).Methods("GET") 1019 t.Router.HandleFunc("/name/{label}/edit", t.editRegistration).Methods("GET") 1020 t.Router.HandleFunc("/name/{label}/edit", t.updateRegistration).Methods("POST") 1021 t.Router.HandleFunc("/search", t.searchPage).Methods("GET") 1022 1023 /* ToS API */ 1024 // t.Router.HandleFunc("/terms", t.termsResponse).Methods("GET") 1025 // t.Router.HandleFunc("/privacy", t.privacyResponse).Methods("GET") 1026 1027 /* Config API */ 1028 t.Router.HandleFunc("/config", t.configResponse).Methods("GET") 1029 1030 /* Assets HTML */ 1031 t.Router.PathPrefix("/css").Handler(http.StripPrefix("/css", http.FileServer(http.Dir(t.getFileName("static/css"))))) 1032 t.Router.PathPrefix("/images").Handler(http.StripPrefix("/images", http.FileServer(http.Dir(t.getFileName("static/images"))))) 1033 } 1034 1035 // Initialize the gnsregistrar instance with cfgfile 1036 func (t *Registrar) Initialize(cfg RegistrarConfig) { 1037 var ( 1038 identityResponse IdentityInfo 1039 err error 1040 ) 1041 t.Cfg = cfg 1042 t.Logger = log.New(os.Stdout, "gnunet-gns-registrar:", log.LstdFlags) 1043 if t.Cfg.Ini.Section("gns-registrar").Key("production").MustBool(false) { 1044 t.Logf(LogInfo, "Production mode enabled\n") 1045 } 1046 navTplFile := t.Cfg.Ini.Section("gns-registrar").Key("nav_template").MustString(t.getFileName("web/templates/nav.html")) 1047 footerTplFile := t.Cfg.Ini.Section("gns-registrar").Key("footer_template").MustString(t.getFileName("web/templates/footer.html")) 1048 landingTplFile := t.Cfg.Ini.Section("gns-registrar").Key("landing_template").MustString(t.getFileName("web/templates/landing.html")) 1049 t.LandingTpl, err = template.ParseFiles(landingTplFile, navTplFile, footerTplFile) 1050 if err != nil { 1051 t.Logf(LogError, "%s\n", err.Error()) 1052 os.Exit(1) 1053 } 1054 nameTplFile := t.Cfg.Ini.Section("gns-registrar").Key("name_template").MustString(t.getFileName("web/templates/name.html")) 1055 t.NameTpl, err = template.ParseFiles(nameTplFile, navTplFile, footerTplFile) 1056 if err != nil { 1057 t.Logf(LogError, err.Error()) 1058 os.Exit(1) 1059 } 1060 buyTplFile := t.Cfg.Ini.Section("gns-registrar").Key("buy_template").MustString(t.getFileName("web/templates/buy.html")) 1061 t.BuyTpl, err = template.ParseFiles(buyTplFile, navTplFile, footerTplFile) 1062 if err != nil { 1063 t.Logf(LogError, "%s\n", err.Error()) 1064 os.Exit(1) 1065 } 1066 editTplFile := t.Cfg.Ini.Section("gns-registrar").Key("edit_template").MustString(t.getFileName("web/templates/edit.html")) 1067 t.EditTpl, err = template.ParseFiles(editTplFile, navTplFile, footerTplFile) 1068 if err != nil { 1069 t.Logf(LogError, "%s\n", err.Error()) 1070 os.Exit(1) 1071 } 1072 paymentExp := t.Cfg.Ini.Section("gns-registrar").Key("payment_required_expiration").MustString("1h") 1073 recordExp := t.Cfg.Ini.Section("gns-registrar").Key("relative_delegation_expiration").MustString("24h") 1074 t.RegistrationExpirationDaysCount = t.Cfg.Ini.Section("gns-registrar").Key("registration_duration_days").MustUint64(5) 1075 t.RelativeRegistrationExpiration, _ = time.ParseDuration(fmt.Sprintf("%dh", t.RegistrationExpirationDaysCount*24)) 1076 t.RelativeDelegationExpiration, _ = time.ParseDuration(recordExp) 1077 t.PaymentExpiration, _ = time.ParseDuration(paymentExp) 1078 costStr := t.Cfg.Ini.Section("gns-registrar").Key("registration_cost").MustString("KUDOS:0.3") 1079 t.RegistrationCost, err = talerutil.ParseAmount(costStr) 1080 if err != nil { 1081 t.Logf(LogError, "Error parsing amount `%s': `%s'\n", costStr, err.Error()) 1082 os.Exit(1) 1083 } 1084 t.BaseUrl = t.Cfg.Ini.Section("gns-registrar").Key("base_url").MustString("http://localhost:11000") 1085 t.SuffixHint = t.Cfg.Ini.Section("gns-registrar").Key("suffix_hint").MustString("example.alt") 1086 t.SummaryTemplateString = t.Cfg.Ini.Section("gns-registrar").Key("order_summary_template").MustString("Registration of `${NAME}' at GNUnet FCFS registrar") 1087 t.RootZoneName = t.Cfg.Ini.Section("gns-registrar").Key("root_zone_name").MustString("test") 1088 t.GnunetUrl = t.Cfg.Ini.Section("gns-registrar").Key("base_url_gnunet").MustString("http://localhost:7776") 1089 t.GnunetBasicAuthEnabled = t.Cfg.Ini.Section("gns-registrar").Key("basic_auth_gnunet_enabled").MustBool(true) 1090 t.GnunetUsername = t.Cfg.Ini.Section("gns-registrar").Key("basic_auth_gnunet_username").MustString("jdoe") 1091 t.GnunetPassword = t.Cfg.Ini.Section("gns-registrar").Key("basic_auth_gnunet_password").MustString("secret") 1092 t.ValidLabelRegex = t.Cfg.Ini.Section("gns-registrar").Key("valid_label_regex").MustString("") 1093 t.ValidLabelScript = t.Cfg.Ini.Section("gns-registrar").Key("valid_label_script").MustString("") 1094 client := &http.Client{} 1095 req, _ := http.NewRequest(http.MethodGet, t.GnunetUrl+"/identity/name/"+t.RootZoneName, nil) 1096 if t.GnunetBasicAuthEnabled { 1097 req.SetBasicAuth(t.GnunetUsername, t.GnunetPassword) 1098 } 1099 resp, err := client.Do(req) 1100 if err != nil { 1101 t.Logf(LogError, "Failed to get zone key. Is gnunet running?\n") 1102 os.Exit(1) 1103 return 1104 } 1105 defer resp.Body.Close() 1106 if http.StatusNotFound == resp.StatusCode { 1107 t.Logf(LogError, "Zone not found.\n") 1108 os.Exit(1) 1109 } else if http.StatusOK == resp.StatusCode { 1110 respData, err := io.ReadAll(resp.Body) 1111 if err != nil { 1112 t.Logf(LogError, "Failed to get zone contents: `%s'\n", err.Error()) 1113 os.Exit(1) 1114 } 1115 err = json.NewDecoder(bytes.NewReader(respData)).Decode(&identityResponse) 1116 if err != nil { 1117 t.Logf(LogError, "Failed to get zone contents: `%s'\n", err.Error()) 1118 os.Exit(1) 1119 } 1120 t.RootZoneKey = identityResponse.Pubkey 1121 } else { 1122 t.Logf(LogError, "Failed to get zone contents\n") 1123 os.Exit(1) 1124 } 1125 t.Merchant = cfg.Merchant 1126 merchConfig, err := t.Merchant.GetConfig() 1127 if nil != err { 1128 t.Logf(LogError, "Failed to get merchant config\n") 1129 os.Exit(1) 1130 } 1131 currencySpec, currencySupported := merchConfig.Currencies[t.RegistrationCost.Currency] 1132 for !currencySupported { 1133 t.Logf(LogError, "Currency `%s' not supported by merchant!\n", t.RegistrationCost.Currency) 1134 os.Exit(1) 1135 } 1136 t.CurrencySpec = currencySpec 1137 t.setupHandlers() 1138 }