ronantakizawa/codereview-bench huggingface dataset:
Repository: syncthing/syncthing
File: lib/config/guiconfiguration.go (Go)

Code before:

	if c.UseTLS() {
		u.Scheme = "https"
	}

	if strings.HasPrefix(u.Host, ":") {
		// Empty host, i.e. ":port", use IPv4 localhost
		u.Host = "127.0.0.1" + u.Host
	} else if strings.HasPrefix(u.Host, "0.0.0.0:") {
		// IPv4 all zeroes host, convert to IPv4 localhost
		u.Host = "127.0.0.1" + u.Host[7:]
	} else if strings.HasPrefix(u.Host, "[::]:") {
		// IPv6 all zeroes host, convert to IPv6 localhost
		u.Host = "[::1]" + u.Host[4:]
	}

	return u.String()
}

// SetHashedPassword hashes the given plaintext password and stores the new hash.
func (c *GUIConfiguration) HashAndSetPassword(password string) error {
	hash, err := bcrypt.GenerateFromPassword([]byte(password), 0)
	if err == nil {
		c.Password = string(hash)
	}
	return err
}

// CompareHashedPassword returns nil when the given plaintext password matches the stored hash.
func (c GUIConfiguration) CompareHashedPassword(password string) error {
	configPasswordBytes := []byte(c.Password)
	passwordBytes := []byte(password)
	return bcrypt.CompareHashAndPassword(configPasswordBytes, passwordBytes)
}

// IsValidAPIKey returns true when the given API key is valid, including both
// the value in config and any overrides
func (c GUIConfiguration) IsValidAPIKey(apiKey string) bool {
	switch apiKey {
	case "":
		return false

	case c.APIKey, os.Getenv("STGUIAPIKEY"):
		return true

	default:
		return false
	}
}

func (c *GUIConfiguration) prepare() {

Review comment (suggestion):
```suggestion
	if err != nil {
		return err
	}
	c.Password = string(hash)
	return nil
```

Otherwise lgtm

Code after:

	if c.UseTLS() {
		u.Scheme = "https"
	}

	if strings.HasPrefix(u.Host, ":") {
		// Empty host, i.e. ":port", use IPv4 localhost
		u.Host = "127.0.0.1" + u.Host
	} else if strings.HasPrefix(u.Host, "0.0.0.0:") {
		// IPv4 all zeroes host, convert to IPv4 localhost
		u.Host = "127.0.0.1" + u.Host[7:]
	} else if strings.HasPrefix(u.Host, "[::]:") {
		// IPv6 all zeroes host, convert to IPv6 localhost
		u.Host = "[::1]" + u.Host[4:]
	}

	return u.String()
}

// SetHashedPassword hashes the given plaintext password and stores the new hash.
func (c *GUIConfiguration) HashAndSetPassword(password string) error {
	hash, err := bcrypt.GenerateFromPassword([]byte(password), 0)
	if err != nil {
		return err
	}
	c.Password = string(hash)
	return nil
}

// CompareHashedPassword returns nil when the given plaintext password matches the stored hash.
func (c GUIConfiguration) CompareHashedPassword(password string) error {
	configPasswordBytes := []byte(c.Password)
	passwordBytes := []byte(password)
	return bcrypt.CompareHashAndPassword(configPasswordBytes, passwordBytes)
}

// IsValidAPIKey returns true when the given API key is valid, including both
// the value in config and any overrides
func (c GUIConfiguration) IsValidAPIKey(apiKey string) bool {
	switch apiKey {
	case "":
		return false

	case c.APIKey, os.Getenv("STGUIAPIKEY"):
		return true

	default:
		return false
	}
}
Repository: syncthing/syncthing
File: cmd/syncthing/generate/generate.go (Go)

Code before:
		if guiUser == "" && guiPassword == "" {
			log.Println("WARNING: Config exists; will not overwrite.")
			return nil
		}

		if cfg, _, err = config.Load(cfgFile, myID, events.NoopLogger); err != nil {
			return errors.Wrap(err, "load config")
		}
	} else {
		if cfg, err = syncthing.DefaultConfig(cfgFile, myID, events.NoopLogger, noDefaultFolder); err != nil {
			return errors.Wrap(err, "create config")
		}
	}

	ctx, cancel := context.WithCancel(context.Background())
	go cfg.Serve(ctx)
	defer cancel()

	guiCfg := cfg.GUI()
	waiter, err := cfg.Modify(func(cfg *config.Configuration) {
		if changed := overrideGUIAuthentication(&guiCfg, guiUser, guiPassword); changed {
			cfg.GUI = guiCfg
		}
	})
	if err != nil {
		return errors.Wrap(err, "modify config")
	}

	waiter.Wait()
	if err := cfg.Save(); err != nil {
		return errors.Wrap(err, "save config")
	}
	return nil
}

func overrideGUIAuthentication(guiCfg *config.GUIConfiguration, guiUser, guiPassword string) bool {
	changed := false
	if guiUser != "" && guiCfg.User != guiUser {
		guiCfg.User = guiUser
		log.Println("Overriding GUI authentication user name:", guiUser)
		changed = true
	}

	if guiPassword != "" && guiCfg.Password != guiPassword {
		if err := guiCfg.HashAndSetPassword(guiPassword); err != nil {
			log.Fatal("Failed to set GUI authentication password.")
		} else {
			log.Println("Overriding GUI authentication password.")
			changed = true
		}
	}

Review comment (suggestion):
```suggestion
		return fmt.Errorf("modify config: %w", err)
```

spelling of this changed in recent Go, I realize this is just copied/moved from the old code but might as well update in the process. (github.com/pkg/errors should not be used in new code)

(Same in all the other `errors.Wrap` calls, I just picked one at random.)

Code after:
			return nil
		}

		if cfg, _, err = config.Load(cfgFile, myID, events.NoopLogger); err != nil {
			return fmt.Errorf("load config: %w", err)
		}
	} else {
		if cfg, err = syncthing.DefaultConfig(cfgFile, myID, events.NoopLogger, noDefaultFolder); err != nil {
			return fmt.Errorf("create config: %w", err)
		}
	}

	ctx, cancel := context.WithCancel(context.Background())
	go cfg.Serve(ctx)
	defer cancel()

	var updateErr error
	waiter, err := cfg.Modify(func(cfg *config.Configuration) {
		updateErr = updateGUIAuthentication(&cfg.GUI, guiUser, guiPassword)
	})
	if err != nil {
		return fmt.Errorf("modify config: %w", err)
	}

	waiter.Wait()
	if updateErr != nil {
		return updateErr
	}
	if err := cfg.Save(); err != nil {
		return fmt.Errorf("save config: %w", err)
	}
	return nil
}

func updateGUIAuthentication(guiCfg *config.GUIConfiguration, guiUser, guiPassword string) error {
	if guiUser != "" && guiCfg.User != guiUser {
		guiCfg.User = guiUser
		log.Println("Updated GUI authentication user name:", guiUser)
	}

	if guiPassword != "" && guiCfg.Password != guiPassword {
		if err := guiCfg.HashAndSetPassword(guiPassword); err != nil {
			return fmt.Errorf("Failed to set GUI authentication password: %w", err)
		}
		log.Println("Updated GUI authentication password.")
	}
	return nil
}
Repository: syncthing/syncthing
File: cmd/syncthing/generate/generate.go (Go)

Code before:
	ctx, cancel := context.WithCancel(context.Background())
	go cfg.Serve(ctx)
	defer cancel()

	guiCfg := cfg.GUI()
	waiter, err := cfg.Modify(func(cfg *config.Configuration) {
		if changed := overrideGUIAuthentication(&guiCfg, guiUser, guiPassword); changed {
			cfg.GUI = guiCfg
		}
	})
	if err != nil {
		return errors.Wrap(err, "modify config")
	}

	waiter.Wait()
	if err := cfg.Save(); err != nil {
		return errors.Wrap(err, "save config")
	}
	return nil
}

func overrideGUIAuthentication(guiCfg *config.GUIConfiguration, guiUser, guiPassword string) bool {
	changed := false
	if guiUser != "" && guiCfg.User != guiUser {
		guiCfg.User = guiUser
		log.Println("Overriding GUI authentication user name:", guiUser)
		changed = true
	}

	if guiPassword != "" && guiCfg.Password != guiPassword {
		if err := guiCfg.HashAndSetPassword(guiPassword); err != nil {
			log.Fatal("Failed to set GUI authentication password.")
		} else {
			log.Println("Overriding GUI authentication password.")
			changed = true
		}
	}
	return changed
}


Review comment (suggestion):
Minor nit, but "Overriding" feels to me like something is being temporarily overridden, not permanently changed. If we need to say anything at all in the success case I'd suggest something like "updated" or "changed".

Code after:
	defer cancel()

	var updateErr error
	waiter, err := cfg.Modify(func(cfg *config.Configuration) {
		updateErr = updateGUIAuthentication(&cfg.GUI, guiUser, guiPassword)
	})
	if err != nil {
		return fmt.Errorf("modify config: %w", err)
	}

	waiter.Wait()
	if updateErr != nil {
		return updateErr
	}
	if err := cfg.Save(); err != nil {
		return fmt.Errorf("save config: %w", err)
	}
	return nil
}

func updateGUIAuthentication(guiCfg *config.GUIConfiguration, guiUser, guiPassword string) error {
	if guiUser != "" && guiCfg.User != guiUser {
		guiCfg.User = guiUser
		log.Println("Updated GUI authentication user name:", guiUser)
	}

	if guiPassword != "" && guiCfg.Password != guiPassword {
		if err := guiCfg.HashAndSetPassword(guiPassword); err != nil {
			return fmt.Errorf("Failed to set GUI authentication password: %w", err)
		}
		log.Println("Updated GUI authentication password.")
	}
	return nil
}
Repository: syncthing/syncthing
File: cmd/syncthing/generate/generate.go (Go)

Code before:

	guiCfg := cfg.GUI()
	waiter, err := cfg.Modify(func(cfg *config.Configuration) {
		if changed := updateGUIAuthentication(&guiCfg, guiUser, guiPassword); changed {
			cfg.GUI = guiCfg
		}
	})
	if err != nil {
		return fmt.Errorf("modify config: %w", err)
	}

	waiter.Wait()
	if err := cfg.Save(); err != nil {
		return fmt.Errorf("save config: %w", err)
	}
	return nil
}

func updateGUIAuthentication(guiCfg *config.GUIConfiguration, guiUser, guiPassword string) bool {
	changed := false
	if guiUser != "" && guiCfg.User != guiUser {
		guiCfg.User = guiUser
		log.Println("Updated GUI authentication user name:", guiUser)
		changed = true
	}

	if guiPassword != "" && guiCfg.Password != guiPassword {
		if err := guiCfg.HashAndSetPassword(guiPassword); err != nil {
			log.Fatal("Failed to set GUI authentication password.")
		} else {
			log.Println("Updated GUI authentication password.")
			changed = true
		}
	}
	return changed
}


Review comment (suggestion):
This is comparing a hash against a plain-text password, isn't it?  
Probably the field should be named `Password` but `PasswordHash`.

Code after:

	var updateErr error
	waiter, err := cfg.Modify(func(cfg *config.Configuration) {
		updateErr = updateGUIAuthentication(&cfg.GUI, guiUser, guiPassword)
	})
	if err != nil {
		return fmt.Errorf("modify config: %w", err)
	}

	waiter.Wait()
	if updateErr != nil {
		return updateErr
	}
	if err := cfg.Save(); err != nil {
		return fmt.Errorf("save config: %w", err)
	}
	return nil
}

func updateGUIAuthentication(guiCfg *config.GUIConfiguration, guiUser, guiPassword string) error {
	if guiUser != "" && guiCfg.User != guiUser {
		guiCfg.User = guiUser
		log.Println("Updated GUI authentication user name:", guiUser)
	}

	if guiPassword != "" && guiCfg.Password != guiPassword {
		if err := guiCfg.HashAndSetPassword(guiPassword); err != nil {
			return fmt.Errorf("Failed to set GUI authentication password: %w", err)
		}
		log.Println("Updated GUI authentication password.")
	}
	return nil
}
Repository: syncthing/syncthing
File: cmd/syncthing/generate/generate.go (Go)

Code before:
		if changed := updateGUIAuthentication(&guiCfg, guiUser, guiPassword); changed {
			cfg.GUI = guiCfg
		}
	})
	if err != nil {
		return fmt.Errorf("modify config: %w", err)
	}

	waiter.Wait()
	if err := cfg.Save(); err != nil {
		return fmt.Errorf("save config: %w", err)
	}
	return nil
}

func updateGUIAuthentication(guiCfg *config.GUIConfiguration, guiUser, guiPassword string) bool {
	changed := false
	if guiUser != "" && guiCfg.User != guiUser {
		guiCfg.User = guiUser
		log.Println("Updated GUI authentication user name:", guiUser)
		changed = true
	}

	if guiPassword != "" && guiCfg.Password != guiPassword {
		if err := guiCfg.HashAndSetPassword(guiPassword); err != nil {
			log.Fatal("Failed to set GUI authentication password.")
		} else {
			log.Println("Updated GUI authentication password.")
			changed = true
		}
	}
	return changed
}


Review comment (suggestion):
The calling function returns errors, so I'd expect this to return an error too to be passed upwards.

Code after:
		updateErr = updateGUIAuthentication(&cfg.GUI, guiUser, guiPassword)
	})
	if err != nil {
		return fmt.Errorf("modify config: %w", err)
	}

	waiter.Wait()
	if updateErr != nil {
		return updateErr
	}
	if err := cfg.Save(); err != nil {
		return fmt.Errorf("save config: %w", err)
	}
	return nil
}

func updateGUIAuthentication(guiCfg *config.GUIConfiguration, guiUser, guiPassword string) error {
	if guiUser != "" && guiCfg.User != guiUser {
		guiCfg.User = guiUser
		log.Println("Updated GUI authentication user name:", guiUser)
	}

	if guiPassword != "" && guiCfg.Password != guiPassword {
		if err := guiCfg.HashAndSetPassword(guiPassword); err != nil {
			return fmt.Errorf("Failed to set GUI authentication password: %w", err)
		}
		log.Println("Updated GUI authentication password.")
	}
	return nil
}
Repository: syncthing/syncthing
File: lib/ignore/ignore_test.go (Go)

Code before:
		}
		if !IsParseError(err) {
			t.Fatal("bad pattern should be a parse error")
		}
	}
}

func TestWindowsLineEndings(t *testing.T) {
	if runtime.GOOS != "windows" {
		t.Skip("Windows specific")
	}

	lines := "foo\nbar\nbaz\n"

	ffs := fs.NewFilesystem(fs.FilesystemTypeBasic, ".")
	m := New(ffs)
	if err := m.Parse(strings.NewReader(lines), ".stignore"); err != nil {
		t.Fatal(err)
	}
	if err := WriteIgnores(ffs, ".stignore", m.Lines()); err != nil {
		t.Fatal(err)
	}

	fd, err := ffs.Open(".stignore")
	if err != nil {
		t.Fatal(err)
	}
	bs, err := io.ReadAll(fd)
	fd.Close()
	if err != nil {
		t.Fatal(err)
	}

	unixLineEndings := bytes.Count(bs, []byte("\n"))
	windowsLineEndings := bytes.Count(bs, []byte("\r\n"))
	if unixLineEndings == 0 || windowsLineEndings != unixLineEndings {
		t.Error("expected there to be a non-zero number of Windows line endings")
	}
}


Review comment (suggestion):
As far as I see this creates an `.stignore` in lib/ignores and doesn't remove it. Generally I'd also prefer `ioutil.TempDir`.

Code after:
		}
		if !IsParseError(err) {
			t.Fatal("bad pattern should be a parse error")
		}
	}
}

func TestWindowsLineEndings(t *testing.T) {
	if runtime.GOOS != "windows" {
		t.Skip("Windows specific")
	}

	lines := "foo\nbar\nbaz\n"

	dir, err := os.MkdirTemp("", "syncthing-test")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(dir)

	ffs := fs.NewFilesystem(fs.FilesystemTypeBasic, dir)
	m := New(ffs)
	if err := m.Parse(strings.NewReader(lines), ".stignore"); err != nil {
		t.Fatal(err)
	}
	if err := WriteIgnores(ffs, ".stignore", m.Lines()); err != nil {
		t.Fatal(err)
	}

	fd, err := ffs.Open(".stignore")
	if err != nil {
		t.Fatal(err)
	}
	bs, err := io.ReadAll(fd)
	fd.Close()
	if err != nil {
		t.Fatal(err)
	}

	unixLineEndings := bytes.Count(bs, []byte("\n"))
	windowsLineEndings := bytes.Count(bs, []byte("\r\n"))
	if unixLineEndings == 0 || windowsLineEndings != unixLineEndings {
		t.Error("expected there to be a non-zero number of Windows line endings")
	}
}
Repository: syncthing/syncthing
File: lib/fs/util.go (Go)

Code before:

	disallowed := `<>:"'/\|?*[]{};:!@$%&^#` + windowsDisallowedCharacters
	prev := ' '
	for _, c := range path {
		if !unicode.IsPrint(c) || c == unicode.ReplacementChar ||
			strings.ContainsRune(disallowed, c) {
			c = ' '
		}

		if !(c == ' ' && prev == ' ') {
			b.WriteRune(c)
		}
		prev = c
	}

	path = strings.TrimSpace(b.String())
	if windowsIsReserved(path) {
		path = "-" + path
	}
	return path
}

func windowsIsReserved(part string) bool {
	upperCased := strings.ToUpper(part)
	for _, disallowed := range windowsDisallowedNames {
		if upperCased == disallowed {
			return true
		}
		if strings.HasPrefix(upperCased, disallowed+".") {
			// nul.txt.jpg is also disallowed
			return true
		}
	}
	return false
}

// IsParent compares paths purely lexicographically, meaning it returns false
// if path and parent aren't both absolute or relative.
func IsParent(path, parent string) bool {
	if parent == path {
		// Twice the same root on windows would not be caught at the end.
		return false
	}
	if filepath.IsAbs(path) != filepath.IsAbs(parent) {
		return false
	}
	if parent == "" || parent == "." {
		// The empty string is the parent of everything except the empty
		// string and ".". (Avoids panic in the last step.)
		return path != "" && path != "."
	}

Review comment (suggestion):
Though rare, `CONIN$` and `CONOUT$` are also reserved words. See
https://docs.microsoft.com/en-us/windows/console/console-handles ,
https://ss64.com/nt/syntax-filenames.html and
https://github.com/syncthing/syncthing/blob/f5106f46815dc793cc168e17dc2bb9db1e43d524/lib/fs/util.go#L58-L63

Code after:
		prev = c
	}

	path = strings.TrimSpace(b.String())
	if windowsIsReserved(path) {
		path = "-" + path
	}
	return path
}

func windowsIsReserved(part string) bool {
	// nul.txt.jpg is also disallowed.
	dot := strings.IndexByte(part, '.')
	if dot != -1 {
		part = part[:dot]
	}

	// Check length to skip allocating ToUpper.
	if len(part) != 3 && len(part) != 4 {
		return false
	}

	// COM0 and LPT0 are missing from the Microsoft docs,
	// but Windows Explorer treats them as invalid too.
	// (https://docs.microsoft.com/windows/win32/fileio/naming-a-file)
	switch strings.ToUpper(part) {
	case "CON", "PRN", "AUX", "NUL",
		"COM0", "COM1", "COM2", "COM3", "COM4",
		"COM5", "COM6", "COM7", "COM8", "COM9",
		"LPT0", "LPT1", "LPT2", "LPT3", "LPT4",
		"LPT5", "LPT6", "LPT7", "LPT8", "LPT9":
		return true
	}
	return false
}

// IsParent compares paths purely lexicographically, meaning it returns false
// if path and parent aren't both absolute or relative.
func IsParent(path, parent string) bool {
	if parent == path {
		// Twice the same root on windows would not be caught at the end.
		return false
	}
	if filepath.IsAbs(path) != filepath.IsAbs(parent) {
		return false
	}
	if parent == "" || parent == "." {
		// The empty string is the parent of everything except the empty
		// string and ".". (Avoids panic in the last step.)
		return path != "" && path != "."
	}
Repository: syncthing/syncthing
File: gui/default/syncthing/core/validDeviceidDirective.js (JavaScript)

Code before:
angular.module('syncthing.core')
    .directive('validDeviceid', function ($http) {
        return {
            require: 'ngModel',
            link: function (scope, elm, attrs, ctrl) {
                ctrl.$parsers.unshift(function (viewValue) {
                    if (scope.currentDevice._editing.indexOf('add') != 0) {
                        // we shouldn't validate
                        ctrl.$setValidity('validDeviceid', true);
                    } else {
                        $http.get(urlbase + '/svc/deviceid?id=' + viewValue).success(function (resp) {
                            if (resp.error) {
                                ctrl.$setValidity('validDeviceid', false);
                            } else {
                                ctrl.$setValidity('validDeviceid', true);
                            }
                        });
                        //Prevents user from adding a duplicate ID
                        if (scope.devices.hasOwnProperty(viewValue)) {
                            ctrl.$setValidity('unique', false);
                        } else {
                            ctrl.$setValidity('unique', true);
                        }
                    }
                    return viewValue;
                });
            }
        };
    });


Review comment (refactor):
Here I'd definitely move the check into a wrapper method.  Within this file, it's totally not obvious what other values starting with "add" may exist.

Or even better, get rid of this check completely.  It's only ever used once and that using element now has `ng-if="currentDevice._editing == 'add'"` as a condition, so this check here is always false.  I don't particularly like tying the code of a reusable (in theory) directive to a specific state variable.

Code after:
angular.module('syncthing.core')
    .directive('validDeviceid', function ($http) {
        return {
            require: 'ngModel',
            link: function (scope, elm, attrs, ctrl) {
                ctrl.$parsers.unshift(function (viewValue) {
                    $http.get(urlbase + '/svc/deviceid?id=' + viewValue).success(function (resp) {
                        if (resp.error) {
                            ctrl.$setValidity('validDeviceid', false);
                        } else {
                            ctrl.$setValidity('validDeviceid', true);
                        }
                    });
                    //Prevents user from adding a duplicate ID
                    if (scope.devices.hasOwnProperty(viewValue)) {
                        ctrl.$setValidity('unique', false);
                    } else {
                        ctrl.$setValidity('unique', true);
                    }
                    return viewValue;
                });
            }
        };
    });
Repository: syncthing/syncthing
File: cmd/stdiscosrv/apisrv.go (Go)

Code before:
			rec.Misses = misses
			rec.Missed = time.Now().UnixNano()
			rec.Addresses = nil
			// rec.Seen retained from get
			s.db.put(key, rec)
		}

		w.Header().Set("Retry-After", notFoundRetryAfterString(int(misses)))
		http.Error(w, "Not Found", http.StatusNotFound)
		return
	}

	lookupRequestsTotal.WithLabelValues("success").Inc()

	bs, _ := json.Marshal(announcement{
		Seen:      time.Unix(0, rec.Seen),
		Addresses: addressStrs(rec.Addresses),
	})
	w.Header().Set("Content-Type", "application/json")
	w.Write(bs)
}

func (s *apiSrv) handlePOST(ctx context.Context, remoteAddr *net.TCPAddr, w http.ResponseWriter, req *http.Request) {
	reqID := ctx.Value(idKey).(requestID)

	rawCert, err := certificateBytes(req)
	if err != nil {
		if debug {
			log.Println(reqID, err.Error())
		}
		announceRequestsTotal.WithLabelValues("no_certificate").Inc()
		w.Header().Set("Retry-After", errorRetryAfterString())
		http.Error(w, "Forbidden", http.StatusForbidden)
		return
	}

	if rawCert == nil {
		if debug {
			log.Println(reqID, "no certificates")
		}
		announceRequestsTotal.WithLabelValues("no_certificate").Inc()
		w.Header().Set("Retry-After", errorRetryAfterString())
		http.Error(w, "Forbidden", http.StatusForbidden)
		return
	}

	var ann announcement
	if err := json.NewDecoder(req.Body).Decode(&ann); err != nil {
		if debug {
			log.Println(reqID, "decode:", err)
		}

Review comment (suggestion):
This is the same as the following `if`, which is now a no-op since the returned certificate will not be nil if the error is non-nil.

Code after:
			rec.Misses = misses
			rec.Missed = time.Now().UnixNano()
			rec.Addresses = nil
			// rec.Seen retained from get
			s.db.put(key, rec)
		}

		w.Header().Set("Retry-After", notFoundRetryAfterString(int(misses)))
		http.Error(w, "Not Found", http.StatusNotFound)
		return
	}

	lookupRequestsTotal.WithLabelValues("success").Inc()

	bs, _ := json.Marshal(announcement{
		Seen:      time.Unix(0, rec.Seen),
		Addresses: addressStrs(rec.Addresses),
	})
	w.Header().Set("Content-Type", "application/json")
	w.Write(bs)
}

func (s *apiSrv) handlePOST(ctx context.Context, remoteAddr *net.TCPAddr, w http.ResponseWriter, req *http.Request) {
	reqID := ctx.Value(idKey).(requestID)

	rawCert, err := certificateBytes(req)
	if err != nil {
		if debug {
			log.Println(reqID, "no certificates:", err)
		}
		announceRequestsTotal.WithLabelValues("no_certificate").Inc()
		w.Header().Set("Retry-After", errorRetryAfterString())
		http.Error(w, "Forbidden", http.StatusForbidden)
		return
	}

	var ann announcement
	if err := json.NewDecoder(req.Body).Decode(&ann); err != nil {
		if debug {
			log.Println(reqID, "decode:", err)
		}
		announceRequestsTotal.WithLabelValues("bad_request").Inc()
		w.Header().Set("Retry-After", errorRetryAfterString())
		http.Error(w, "Bad Request", http.StatusBadRequest)
		return
	}

	deviceID := protocol.NewDeviceID(rawCert)

	addresses := fixupAddresses(remoteAddr, ann.Addresses)
	if len(addresses) == 0 {