Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Invalid Value for keyword 'ip' = ' #1

Closed
bencdll opened this issue Mar 10, 2022 · 3 comments
Closed

Invalid Value for keyword 'ip' = ' #1

bencdll opened this issue Mar 10, 2022 · 3 comments

Comments

@bencdll
Copy link

bencdll commented Mar 10, 2022

I am following your guide exactly but the DNS record on Cloudflare never updates.
Model: USG-Pro-4
Version: 4.4.56.5449062

relevant log in /var/log/messages:
WARNING: file /var/cache/ddclient/ddclient_eth2.cache, line 3: Invalid Value for keyword 'ip' = '

@UnchartedBull
Copy link
Contributor

Don't know whether that's still an issue for you, in case it is here is how I solved it:

  • The worker code had some errors if you're using dyndns as a provider. Most of the stuff is corrected with PR Works with service type "dyndns" #2. The responses were invalid though, so Unifi was never able to mark dyndns as successful (return status needs to be good or nochg. Here is the code I'm using that is working fine for me (expand spoiler):
Worker Code
/**
* Receives a HTTP request and replies with a response.
* @param {Request} request
* @returns {Promise<Response>}
*/
async function handleRequest(request) {
const { protocol, pathname } = new URL(request.url);

// Require HTTPS (TLS) connection to be secure.
if (
  "https:" !== protocol ||
  "https" !== request.headers.get("x-forwarded-proto")
) {
  throw new BadRequestException("Please use a HTTPS connection.");
}

switch (pathname) {
  case "/nic/update": {
    if (request.headers.has("Authorization")) {
      const { username, password } = basicAuthentication(request);

      // Throws exception when query parameters aren't formatted correctly
      const url = new URL(request.url);
      verifyParameters(url);

      // Only returns this response when no exception is thrown.
      const response = await informAPI(url, username, password);
      return response;
    }

    throw new BadRequestException("Please provide valid credentials.");
  }

  case "/favicon.ico":
  case "/robots.txt":
    return new Response(null, { status: 204 });
}

return new Response(null, { status: 404 });
}

/**
* Pass the request info to the Cloudflare API Handler
* @param {URL} url
* @param {String} name
* @param {String} token
* @returns {Promise<Response>}
*/
async function informAPI(url, name, token) {
// Parse Url
const hostname = url.searchParams.get("hostname");
const ip = url.searchParams.get("myip");

// Initialize API Handler
const cloudflare = new Cloudflare({
  token: token,
});

const zone = await cloudflare.findZone(name);
const record = await cloudflare.findRecord(zone, hostname);

if (record.content != ip) {
  const result = await cloudflare.updateRecord(record, ip);

  // return good if ip was changed
  return new Response("good " + ip, {
    status: 200,
    headers: {
      "Content-Type": "text/plain;charset=UTF-8",
      "Cache-Control": "no-store"
    },
  });
}

// return nochg if ip wasn't changed
return new Response("nochg " + ip, {
    status: 200,
    headers: {
      "Content-Type": "text/plain;charset=UTF-8",
      "Cache-Control": "no-store"
    },
  });
}

/**
* Throws exception on verification failure.
* @param {string} url
* @throws {UnauthorizedException}
*/
function verifyParameters(url) {
if (!url.searchParams) {
  throw new BadRequestException("You must include proper query parameters");
}

if (!url.searchParams.get("hostname")) {
  throw new BadRequestException("You must specify a hostname");
}

if (!url.searchParams.get("myip")) {
  throw new BadRequestException("You must specify an ip address");
}
}

/**
* Parse HTTP Basic Authorization value.
* @param {Request} request
* @throws {BadRequestException}
* @returns {{ user: string, pass: string }}
*/
function basicAuthentication(request) {
const Authorization = request.headers.get("Authorization");

const [scheme, encoded] = Authorization.split(" ");

// Decodes the base64 value and performs unicode normalization.
// @see https://dev.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/normalize
const buffer = Uint8Array.from(atob(encoded), (character) =>
  character.charCodeAt(0)
);
const decoded = new TextDecoder().decode(buffer).normalize();

// The username & password are split by the first colon.
//=> example: "username:password"
const index = decoded.indexOf(":");

// The user & password are split by the first colon and MUST NOT contain control characters.
// @see https://tools.ietf.org/html/rfc5234#appendix-B.1 (=> "CTL = %x00-1F / %x7F")
if (index === -1 || /[\0-\x1F\x7F]/.test(decoded)) {
  throw new BadRequestException("Invalid authorization value.");
}

return {
  username: decoded.substring(0, index),
  password: decoded.substring(index + 1),
};
}

class UnauthorizedException {
constructor(reason) {
  this.status = 401;
  this.statusText = "badauth";
  this.reason = reason;
}
}

class BadRequestException {
constructor(reason) {
  this.status = 400;
  this.statusText = "Bad Request";
  this.reason = reason;
}
}

class Cloudflare {
constructor(options) {
  this.cloudflare_url = "https://api.cloudflare.com/client/v4";

  if (options.token) {
    this.token = options.token;
  }

  this.findZone = async (name) => {
    var response = await fetch(
      `https://api.cloudflare.com/client/v4/zones?name=${name}`,
      {
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${this.token}`,
        },
      }
    );
    var body = await response.json();
    return body.result[0];
  };

  this.findRecord = async (zone, name) => {
    var response = await fetch(
      `https://api.cloudflare.com/client/v4/zones/${zone.id}/dns_records?name=${name}`,
      {
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${this.token}`,
        },
      }
    );
    var body = await response.json();
    return body.result[0];
  };

  this.updateRecord = async (record, value) => {
    record.content = value;
    var response = await fetch(
      `https://api.cloudflare.com/client/v4/zones/${record.zone_id}/dns_records/${record.id}`,
      {
        method: "PUT",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${this.token}`,
        },
        body: JSON.stringify(record),
      }
    );
    var body = await response.json();
    return body.result[0];
  };
}
}

addEventListener("fetch", (event) => {
event.respondWith(
  handleRequest(event.request).catch((err) => {
    const message = err.reason || err.stack || "Unknown Error";

    return new Response(message, {
      status: err.status || 500,
      statusText: err.statusText || null,
      headers: {
        "Content-Type": "text/plain;charset=UTF-8",
        // Disables caching by default.
        "Cache-Control": "no-store",
        // Returns the "Content-Length" header for HTTP HEAD requests.
        "Content-Length": message.length,
      },
    });
  })
);
});
  • Configuring UniFi OS Step 3:
    • Service: choose dyndns
    • Hostname: the full subdomain and hostname of the record you want to update (e.g. subdomain.mydomain.com, if you want to update the root domain enter the full root domain e.g. mydomain.com, @ won't work here)
    • Username: the domain name containing the record (e.g. mydomain.com)
    • Password: the Cloudflare API Token you created earlier
    • Server: the Cloudflare Worker route <worker-name>.<worker-subdomain>.workers.dev (worker-name isn't necessarily ddns, you can copy this URL from the Routes field in your Cloudflare Worker Details View)

This is how the dialog looks to me:
Screenshot 2022-03-22 at 17 44 29

@willswire
Copy link
Owner

@UnchartedBull thanks for posting the fixes here. I just merged PR #2 - would you mind also submitting a PR with the fixes you've implemented above?

@willswire
Copy link
Owner

@bencdll, this issue has been fixed thanks to #3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants