Skip to content
This repository has been archived by the owner on Dec 16, 2023. It is now read-only.

Commit

Permalink
Reewrite docker registry and new function to http
Browse files Browse the repository at this point in the history
  • Loading branch information
Sirherobrine23 committed Apr 18, 2023
1 parent 06b8646 commit 9b43233
Show file tree
Hide file tree
Showing 9 changed files with 498 additions and 350 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ node_modules/

# test files
examples/souces/
**/test*/*.log
*.deb
*.tar.*
*.tar
Expand Down
164 changes: 109 additions & 55 deletions packages/docker/src/auth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import path from "node:path";
import { parseImage } from "./image.js";
import http from "@sirherobrine23/http";

export type tokenAction = "pull"|"push";
export type tokenAction = "pull"|"push"|"pull,push";
export interface userAuth {
/** Username */
username: string;
Expand All @@ -17,76 +18,129 @@ function basicAuth(username: string, pass?: string) {
}

export class Auth {
constructor (public img: parseImage) {}
#act: tokenAction = "pull";
setAction(act: tokenAction) {
if (act !== this.#act) this.access_token = this.expires_in = this.issued_at = this.token = undefined;
if (act === "pull,push") this.#act = "pull,push";
else if (act === "push") this.#act = "push";
else this.#act = "pull";
return this;
}

has(act: tokenAction) {
return this.#act === act;
}

#auth: userAuth;
#image: parseImage;
#action: tokenAction;
actionHas(rec: tokenAction) {return this.#action === rec;}
setAuth(auth: userAuth) {
if (!(auth.username && auth.password)) throw new Error("Set valid auth");
this.#auth = auth;
return this;
}

token: string;
access_token?: string;
expires_in?: number;
issued_at?: string;

constructor(img: parseImage, tokenAction?: tokenAction, auth?: userAuth) {
this.#image = img;
this.#auth = auth;
if (tokenAction === "push") this.#action = "push";
else this.#action = "pull";
#setScope(www: string) {
www = www.slice(www.indexOf(" ")).trim();
const scopes: {[keyname: string]: string} = {};
while (www.length > 0) {
let indexOf: number;
if ((indexOf = www.indexOf("=")) !== -1) {
const key = www.slice(0, indexOf);
www = www.slice(indexOf+1);
if (www.indexOf("\",") !== -1) {
scopes[key] = www.slice(1, www.indexOf("\",")+1);
www = www.slice(www.indexOf("\",")+2);
} else {
scopes[key] = www;
www = "";
}
if (scopes[key].endsWith("\"")) scopes[key] = scopes[key].slice(0, scopes[key].length -1);
if (scopes[key].startsWith("\"")) scopes[key] = scopes[key].slice(1);
}
}
this.img.realm = scopes.realm;
this.img.service = scopes.service;
return scopes;
}

/**
*
* @param scp - Set custom scope
* @returns
*/
async setup(scp?: string) {
if (this.token) return this;
if (this.#auth?.username) if (!this.#auth.password) throw new TypeError("required Auth.auth.password to login in registry");
const headers = await http.jsonRequest(`http://${this.#image.registry}/v2/`).then(d => d.headers).catch(async (err: http.httpCoreError) => err.headers);
const { owner, repo } = this.#image;
let auth: string = (headers["www-authenticate"] as any);
if (typeof auth === "string" && (auth = auth.trim()).length > 0) {
const scopes = auth.slice(auth.indexOf(" ")).trim().split(",").reduce((acc, data) => {const indexP = data.indexOf("="); if (indexP === -1) return acc; acc[data.slice(0, indexP)] = data.slice(indexP+1).replace(/"(.*)"/, "$1").trim(); return acc;}, {} as {[keyname: string]: string});
this.#image.realm = scopes.realm;
this.#image.service = scopes.service;
// if (scopes.scope) scope = scopes.scope;
async request<T = any>(requestConfig: Omit<http.requestOptions, "body"|"url"> & {reqPath: string|(string[]), body?: () => any|Promise<any>}, scope?: string): Promise<http.dummyRequestResponse<T>> {
let req: http.dummyRequestResponse;
while (true) {
req = await http.dummyRequest<T>({
...requestConfig,
body: typeof requestConfig.body === "function" ? requestConfig.body() : undefined,
url: new URL(typeof requestConfig.reqPath === "string" ? requestConfig.reqPath : path.posix.join(...requestConfig.reqPath), `http://${this.img.registry}`),
headers: {
...requestConfig.headers,
// ...(typeof this.access_token === "string" ? {Authorization: "Bearer "+this.access_token} : typeof this.token === "string" ? {Authorization: "Bearer "+this.token} : {}),
...(typeof this.token === "string" ? {Authorization: "Bearer "+this.token} : {}),
},
});
if (!(req.statusMessage === "write EPIPE" || req.statusMessage === "read ECONNRESET")) break;
}
let scope = scp ?? `repository:${owner}/${repo}:${this.#action}`;

const options: Omit<http.requestOptions, "url"> = {
query: {
service: this.#image.service,
scope
},
headers: {
...(this.#auth?.username && this.#auth?.password ? {Authorization: basicAuth(this.#auth.username, this.#auth.password)} : {}),
},
}
if (req.statusCode === 401) {
scope ||= `repository:${this.img.owner}/${this.img.repo}:${this.#act}`;
if (typeof req.headers["www-authenticate"] === "string") {
const scopes = this.#setScope(req.headers["www-authenticate"]);
if (scopes.scope) {
this.access_token = this.expires_in = this.issued_at = this.token = undefined;
scope = scopes.scope;
}
}
const realme = new URL(this.img.realm || new URL("/token", req.url));
let auth: http.dummyRequestResponse;
while (true) {
auth = await http.jsonDummyRequest({
url: realme,
query: {
service: this.img.service,
scope
},
headers: {
...(this.#auth?.username && this.#auth?.password ? {Authorization: basicAuth(this.#auth.username, this.#auth.password)} : {}),
}
});
if (!(auth.statusMessage === "write EPIPE" || auth.statusMessage === "read ECONNRESET")) break;
}

try {
const { body: { token, access_token, expires_in, issued_at } } = await http.jsonRequest(this.#image.realm ?? `http://${this.#image.registry}/token`, options).catch((err: http.httpCoreError) => {
let d: any;
if (err.body?.details) throw new Error(err.body.details);
else if (err.body?.errors) if (d = err.body.errors.find(d => !!d.message)?.message) d = new Error(d);
throw d ?? err;
});
this.token = token;
if (auth.statusCode !== 200) throw auth;
if (!auth.body.token) throw new Error("Cannot get token!");
this.token = auth.body.token;
const { access_token, expires_in, issued_at } = auth.body;
this.access_token = access_token;
this.expires_in = expires_in;
this.issued_at = issued_at;
if (typeof expires_in === "number" && expires_in >= 1) {
setTimeout(() => {
Object.defineProperty(this, "token", {
configurable: false,
writable: false,
value: new TypeError("Token exired"),
});
}, expires_in);
if (typeof expires_in === "number" && expires_in >= 1) setTimeout(() => this.token = undefined, expires_in);
let dumm: http.dummyRequestResponse;
while (true) {
dumm = await http.dummyRequest<T>({
...requestConfig,
body: typeof requestConfig.body === "function" ? requestConfig.body() : undefined,
url: new URL(typeof requestConfig.reqPath === "string" ? requestConfig.reqPath : path.posix.join(...requestConfig.reqPath), `http://${this.img.registry}`),
headers: {
...requestConfig.headers,
Authorization: "Bearer "+(/*this.access_token||*/this.token),
},
});
if (!(dumm.statusMessage === "write EPIPE" || dumm.statusMessage === "read ECONNRESET")) break;
}
if (dumm.statusCode === 401) {
this.access_token = this.expires_in = this.issued_at = this.token = undefined;
if (typeof dumm.headers["www-authenticate"] === "string") {
const scopes = this.#setScope(dumm.headers["www-authenticate"]);
if (scopes.scope) scope = scopes.scope;
}
return this.request(requestConfig, scope);
}
} catch (err) {
if (err.httpCode !== 404) throw err;
return dumm;
}

return this;
return req;
}
}
}
4 changes: 3 additions & 1 deletion packages/docker/src/image.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isIP } from "node:net";

export class parseImage {
/** service for scope */
service?: string = "registry.docker.io";
Expand Down Expand Up @@ -28,7 +30,7 @@ export class parseImage {
if (split.length === 1) {
this.owner = "library";
this.repo = split.at(0);
} else if (split.length === 2) {
} else if (split.length === 2 && !(split.at(0).includes(":") || Boolean(isIP(split.at(0))) || (/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/).test(split.at(0)))) {
this.owner = split.at(0);
this.repo = split.at(1);
} else {
Expand Down
Loading

0 comments on commit 9b43233

Please sign in to comment.