feat(git_ssh): make ssh clone/pull/push work again@@ -1,4 +1,4 @@
-FROM node:alpine as builder
+FROM node:alpine AS builder
ENV NODE_ENV=development
ENV NODE_OPTIONS=--openssl-legacy-provider
@@ -52,7 +52,7 @@ RUN yarn bundle:islands # Bundle Islands (react-monolith) to ESM/CJS/UMD
COPY ./public /usr/src/app/public
COPY ./app.manifest.json /usr/src/app/app.manifest.json
-FROM node:slim as base
+FROM node:slim AS base
ENV NODE_ENV=production
ENV NODE_OPTIONS=--openssl-legacy-provider
@@ -153,9 +153,14 @@ RUN chmod +x git-shell-commands/no-interactive-login
# Add ssh command to force client command
COPY ./data/ssh_command /usr/bin/
COPY ./data/ssh_command_node /usr/bin/
+COPY ./data/http_client.js /usr/bin/
RUN chmod +x /usr/bin/ssh_command
RUN chmod +x /usr/bin/ssh_command_node
+RUN touch /opt/ssh_commands.log
+RUN chown git:git /opt/ssh_commands.log
+RUN chmod 644 /opt/ssh_commands.log
+
# Setup ssh folder and keys
RUN mkdir -p .ssh
RUN chmod 700 .ssh
@@ -7,7 +7,7 @@ import { makeGitServerService } from "../services/gitServer";
const onSSHAuth: ReqHandler<AppRouteParams, AppRoute.SSH_AUTH> = async (
request,
- reply
+ reply,
) => {
const gitService = makeGitServerService({
request,
@@ -19,10 +19,14 @@ const onSSHAuth: ReqHandler<AppRouteParams, AppRoute.SSH_AUTH> = async (
const { command, repoSlug, username, publicKey } = request.body;
+ console.log("SSH auth request received with body:", request.body);
+
const result = await gitService.repositoryResolver(
- repoSlug.replace(/\.git$/, "")
+ repoSlug.replace(/\.git$/, ""),
);
+ console.log("result from repositoryResolver:", result);
+
let { authMode, gitRepositoryDir } = result;
gitRepositoryDir = gitRepositoryDir.toString().endsWith(".git")
? gitRepositoryDir
@@ -33,6 +37,12 @@ const onSSHAuth: ReqHandler<AppRouteParams, AppRoute.SSH_AUTH> = async (
(authMode === GitServer.AuthMode.PUSH_ONLY &&
command !== "git-receive-pack") // push
) {
+ console.log("Successful auth:", {
+ authMode,
+ command,
+ gitRepositoryDir,
+ });
+
reply.status(200).send({
success: true,
authMode,
@@ -47,9 +57,11 @@ const onSSHAuth: ReqHandler<AppRouteParams, AppRoute.SSH_AUTH> = async (
{
username,
password: publicKey,
- }
+ },
);
+ console.log("is auth valid?", isAuthorizationValid);
+
if (isAuthorizationValid) {
const [orgSlug, repoName] = repoSlug.replace(/\.git$/, "").split("/");
request.prisma.repository
@@ -107,13 +107,14 @@ const RepositoryTreeView: ReactIsland<
title={"Branch"}
onChange={(e) => {
console.log("branch changed to: ", e.currentTarget.value);
- window.__router.push(
- buildRouteLink(AppRoute.REPOSITORY_BROWSER_WITH_PATH, {
+ window.location.href = buildRouteLink(
+ AppRoute.REPOSITORY_BROWSER_WITH_PATH,
+ {
orgSlug: orgSlug,
repoSlug: repoSlug,
currentRef: e.currentTarget.value,
"*": currentPath === "/" ? "" : currentPath,
- }),
+ },
);
}}
style={{
@@ -18,6 +18,12 @@ const makeAuthorizationResolver: ServiceMethodFactory<
const repoSlug = repoSlugUnsafe.replace(/\.(git|pub)$/, "");
+ console.log("AuthorizationResolver called with:", {
+ repoPath,
+ username,
+ isPubKeyAuth,
+ });
+
const user = await request.prisma.user.findUnique({
where: {
username,
@@ -105,13 +105,14 @@ const RepositoryBrowserView: ReactView<RepositoryBrowserViewProps> = ({
title={"Branch"}
onChange={(e) => {
console.log("branch changed to: ", e.currentTarget.value);
- window.__router.push(
- buildRouteLink(AppRoute.REPOSITORY_BROWSER_WITH_PATH, {
+ window.location.href = buildRouteLink(
+ AppRoute.REPOSITORY_BROWSER_WITH_PATH,
+ {
orgSlug: parentOrg.slug,
repoSlug: repo.slug,
currentRef: e.currentTarget.value,
"*": path,
- }),
+ },
);
}}
style={{
@@ -0,0 +1,38 @@
+declare module "./http_client" {
+ import { IncomingMessage } from "http";
+
+ export class HttpResponse {
+ readonly statusCode: number;
+ readonly statusText: string;
+ readonly ok: boolean;
+ readonly headers: IncomingMessage["headers"];
+
+ constructor(incoming: IncomingMessage);
+
+ text(): Promise<string>;
+ isJson(): Promise<boolean>;
+ json(): Promise<any>;
+ }
+
+ export interface RequestConfig {
+ headers?: Record<string, string>;
+ body?: string | Buffer | any;
+ }
+
+ export class HttpClient {
+ constructor();
+
+ get(url: string, config?: RequestConfig): Promise<HttpResponse>;
+ post(url: string, config?: RequestConfig): Promise<HttpResponse>;
+ put(url: string, config?: RequestConfig): Promise<HttpResponse>;
+ patch(url: string, config?: RequestConfig): Promise<HttpResponse>;
+ delete(url: string, config?: RequestConfig): Promise<HttpResponse>;
+ head(url: string, config?: RequestConfig): Promise<HttpResponse>;
+ options(url: string, config?: RequestConfig): Promise<HttpResponse>;
+ custom(
+ method: string,
+ url: string,
+ config?: RequestConfig,
+ ): Promise<HttpResponse>;
+ }
+}
@@ -0,0 +1,204 @@
+/**
+ * Minimal HTTP client using only Node.js built-ins.
+ *
+ * Exports:
+ * - HttpClient: class with methods get/post/put/patch/delete/head/options/custom
+ * - HttpResponse: response wrapper (left unchanged per request)
+ *
+ * HttpResponse API:
+ * - statusCode: number
+ * - statusText: string
+ * - ok: boolean (2xx)
+ * - headers: object
+ * - text(): Promise<string>
+ * - isJson(): Promise<boolean>
+ * - json(): Promise<any>
+ *
+ * HttpClient usage (example):
+ * const { HttpClient } = require('./client');
+ * const client = new HttpClient();
+ * const res = await client.get('http://localhost:3000/auth', {
+ * headers: { 'content-type':'application/json' },
+ * // body: ..., // .post/.put/.patch only
+ * });
+ * if (!res.ok) {
+ * const txt = await res.text();
+ * return txt;
+ * }
+ * if (!await res.isJson()) {
+ * const txt = await res.text();
+ * return txt;
+ * }
+ * const obj = await res.json();
+ * // ... whatever with obj ...
+ */
+
+const { request } = require("http");
+const { URL } = require("url");
+
+class HttpResponse {
+ /**
+ * Wrap an IncomingMessage.
+ * @param {import('http').IncomingMessage} incoming
+ */
+ constructor(incoming) {
+ this._incoming = incoming;
+ this._buf = null;
+ this.statusCode = incoming.statusCode || 0;
+ this.statusText = incoming.statusMessage || "";
+ this.ok = this.statusCode >= 200 && this.statusCode < 300;
+ this.headers = incoming.headers;
+ this.header = incoming.header;
+ this._json = null;
+ }
+
+ /**
+ * Buffer the full response body (private).
+ * @returns {Promise<Buffer>}
+ */
+ async #readAll() {
+ return new Promise((resolve, reject) => {
+ const bufs = [];
+ this._incoming.on("data", (b) => bufs.push(b));
+ this._incoming.on("end", () => resolve(Buffer.concat(bufs)));
+ this._incoming.on("error", reject);
+ });
+ }
+
+ /**
+ * Return the response body as a string. Buffers on first call and reuses the value.
+ * @returns {Promise<string>}
+ */
+ async text() {
+ if (!this._buf) this._buf = await this.#readAll();
+ return this._buf.toString();
+ }
+
+ /**
+ * Feature-detect whether the body is valid JSON.
+ * Uses text() as the single source-of-truth for the buffered body.
+ * @returns {Promise<boolean>}
+ */
+ async isJson() {
+ try {
+ this._json = JSON.parse(await this.text());
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
+ /**
+ * Parse and return JSON body. Throws if body is not JSON.
+ * @returns {Promise<any>}
+ */
+ async json() {
+ if ((await this.isJson()) === false) throw new Error("not json");
+ if (this._json == null) this._json = JSON.parse(await this.text());
+ return this._json;
+ }
+}
+
+class HttpClient {
+ /**
+ * Internal request factory that performs a request and returns HttpResponse.
+ * Private by design: use convenience methods (get/post/...) instead.
+ *
+ * @param {string} method HTTP method
+ * @param {string} url Request URL
+ * @param {{ headers?: Record<string,string>, body?: string|Buffer|any }} [config]
+ * @returns {Promise<HttpResponse>}
+ */
+ async #requestFactory(method, url, config = {}) {
+ const u = new URL(url);
+ const req = request({
+ method,
+ protocol: u.protocol,
+ hostname: u.hostname,
+ port: u.port,
+ path: u.pathname + u.search,
+ headers: config.headers || {},
+ });
+
+ // write body if provided
+ if (config.body) {
+ if (typeof config.body === "string" || Buffer.isBuffer(config.body)) {
+ req.write(config.body);
+ } else {
+ req.write(JSON.stringify(config.body));
+ }
+ }
+
+ req.end();
+
+ const incoming = await new Promise((resolve, reject) => {
+ req.on("error", reject);
+ req.on("response", resolve);
+ });
+
+ return new HttpResponse(incoming);
+ }
+
+ // Convenience methods for common HTTP verbs:
+
+ /**
+ * GET request
+ * @param {string} url
+ * @param {{ headers?: Record<string,string>, body?: string|Buffer|any }} [config]
+ */
+ get(url, config) {
+ return this.#requestFactory("GET", url, config);
+ }
+
+ /**
+ * POST request
+ */
+ post(url, config) {
+ return this.#requestFactory("POST", url, config);
+ }
+
+ /**
+ * PUT request
+ */
+ put(url, config) {
+ return this.#requestFactory("PUT", url, config);
+ }
+
+ /**
+ * PATCH request
+ */
+ patch(url, config) {
+ return this.#requestFactory("PATCH", url, config);
+ }
+
+ /**
+ * DELETE request
+ */
+ delete(url, config) {
+ return this.#requestFactory("DELETE", url, config);
+ }
+
+ /**
+ * HEAD request
+ */
+ head(url, config) {
+ return this.#requestFactory("HEAD", url, config);
+ }
+
+ /**
+ * OPTIONS request
+ */
+ options(url, config) {
+ return this.#requestFactory("OPTIONS", url, config);
+ }
+
+ /**
+ * Custom method
+ * @param {string} method
+ */
+ custom(method, url, config) {
+ return this.#requestFactory(method, url, config);
+ }
+}
+
+module.exports = { HttpClient, HttpResponse };
445ac30 (parent 1ec50a1)5/10/2026, 8:36:54 AMfeat(git_ssh): make ssh clone/pull/push work again+ 368- 89Dockerfile@@ -1,4 +1,4 @@
-FROM node:alpine as builder
+FROM node:alpine AS builder
ENV NODE_ENV=development
ENV NODE_OPTIONS=--openssl-legacy-provider
...@@ -52,7 +52,7 @@ RUN yarn bundle:islands # Bundle Islands (react-monolith) to ESM/CJS/UMD
COPY ./public /usr/src/app/public
COPY ./app.manifest.json /usr/src/app/app.manifest.json
-FROM node:slim as base
+FROM node:slim AS base
ENV NODE_ENV=production
ENV NODE_OPTIONS=--openssl-legacy-provider
...@@ -153,9 +153,14 @@ RUN chmod +x git-shell-commands/no-interactive-login
# Add ssh command to force client command
COPY ./data/ssh_command /usr/bin/
COPY ./data/ssh_command_node /usr/bin/
+COPY ./data/http_client.js /usr/bin/
RUN chmod +x /usr/bin/ssh_command
RUN chmod +x /usr/bin/ssh_command_node
+RUN touch /opt/ssh_commands.log
+RUN chown git:git /opt/ssh_commands.log
+RUN chmod 644 /opt/ssh_commands.log
+
# Setup ssh folder and keys
RUN mkdir -p .ssh
RUN chmod 700 .ssh
app/controllers/ssh-auth.ts@@ -7,7 +7,7 @@ import { makeGitServerService } from "../services/gitServer";
const onSSHAuth: ReqHandler<AppRouteParams, AppRoute.SSH_AUTH> = async (
request,
- reply
+ reply,
) => {
const gitService = makeGitServerService({
request,
...@@ -19,10 +19,14 @@ const onSSHAuth: ReqHandler<AppRouteParams, AppRoute.SSH_AUTH> = async (
const { command, repoSlug, username, publicKey } = request.body;
+ console.log("SSH auth request received with body:", request.body);
+
const result = await gitService.repositoryResolver(
- repoSlug.replace(/\.git$/, "")
+ repoSlug.replace(/\.git$/, ""),
);
+ console.log("result from repositoryResolver:", result);
+
let { authMode, gitRepositoryDir } = result;
gitRepositoryDir = gitRepositoryDir.toString().endsWith(".git")
? gitRepositoryDir
...@@ -33,6 +37,12 @@ const onSSHAuth: ReqHandler<AppRouteParams, AppRoute.SSH_AUTH> = async (
(authMode === GitServer.AuthMode.PUSH_ONLY &&
command !== "git-receive-pack") // push
) {
+ console.log("Successful auth:", {
+ authMode,
+ command,
+ gitRepositoryDir,
+ });
+
reply.status(200).send({
success: true,
authMode,
...@@ -47,9 +57,11 @@ const onSSHAuth: ReqHandler<AppRouteParams, AppRoute.SSH_AUTH> = async (
{
username,
password: publicKey,
- }
+ },
);
+ console.log("is auth valid?", isAuthorizationValid);
+
if (isAuthorizationValid) {
const [orgSlug, repoName] = repoSlug.replace(/\.git$/, "").split("/");
request.prisma.repository
app/islands/RepositoryTreeView.tsx@@ -107,13 +107,14 @@ const RepositoryTreeView: ReactIsland<
title={"Branch"}
onChange={(e) => {
console.log("branch changed to: ", e.currentTarget.value);
- window.__router.push(
- buildRouteLink(AppRoute.REPOSITORY_BROWSER_WITH_PATH, {
+ window.location.href = buildRouteLink(
+ AppRoute.REPOSITORY_BROWSER_WITH_PATH,
+ {
orgSlug: orgSlug,
repoSlug: repoSlug,
currentRef: e.currentTarget.value,
"*": currentPath === "/" ? "" : currentPath,
- }),
+ },
);
}}
style={{
app/services/gitServer/authorizationResolver.ts@@ -18,6 +18,12 @@ const makeAuthorizationResolver: ServiceMethodFactory<
const repoSlug = repoSlugUnsafe.replace(/\.(git|pub)$/, "");
+ console.log("AuthorizationResolver called with:", {
+ repoPath,
+ username,
+ isPubKeyAuth,
+ });
+
const user = await request.prisma.user.findUnique({
where: {
username,
app/views/repository/RepositoryBrowserView.tsx@@ -105,13 +105,14 @@ const RepositoryBrowserView: ReactView<RepositoryBrowserViewProps> = ({
title={"Branch"}
onChange={(e) => {
console.log("branch changed to: ", e.currentTarget.value);
- window.__router.push(
- buildRouteLink(AppRoute.REPOSITORY_BROWSER_WITH_PATH, {
+ window.location.href = buildRouteLink(
+ AppRoute.REPOSITORY_BROWSER_WITH_PATH,
+ {
orgSlug: parentOrg.slug,
repoSlug: repo.slug,
currentRef: e.currentTarget.value,
"*": path,
- }),
+ },
);
}}
style={{
new filedata/http_client.d.ts@@ -0,0 +1,38 @@
+declare module "./http_client" {
+ import { IncomingMessage } from "http";
+
+ export class HttpResponse {
+ readonly statusCode: number;
+ readonly statusText: string;
+ readonly ok: boolean;
+ readonly headers: IncomingMessage["headers"];
+
+ constructor(incoming: IncomingMessage);
+
+ text(): Promise<string>;
+ isJson(): Promise<boolean>;
+ json(): Promise<any>;
+ }
+
+ export interface RequestConfig {
+ headers?: Record<string, string>;
+ body?: string | Buffer | any;
+ }
+
+ export class HttpClient {
+ constructor();
+
+ get(url: string, config?: RequestConfig): Promise<HttpResponse>;
+ post(url: string, config?: RequestConfig): Promise<HttpResponse>;
+ put(url: string, config?: RequestConfig): Promise<HttpResponse>;
+ patch(url: string, config?: RequestConfig): Promise<HttpResponse>;
+ delete(url: string, config?: RequestConfig): Promise<HttpResponse>;
+ head(url: string, config?: RequestConfig): Promise<HttpResponse>;
+ options(url: string, config?: RequestConfig): Promise<HttpResponse>;
+ custom(
+ method: string,
+ url: string,
+ config?: RequestConfig,
+ ): Promise<HttpResponse>;
+ }
+}
new filedata/http_client.js@@ -0,0 +1,204 @@
+/**
+ * Minimal HTTP client using only Node.js built-ins.
+ *
+ * Exports:
+ * - HttpClient: class with methods get/post/put/patch/delete/head/options/custom
+ * - HttpResponse: response wrapper (left unchanged per request)
+ *
+ * HttpResponse API:
+ * - statusCode: number
+ * - statusText: string
+ * - ok: boolean (2xx)
+ * - headers: object
+ * - text(): Promise<string>
+ * - isJson(): Promise<boolean>
+ * - json(): Promise<any>
+ *
+ * HttpClient usage (example):
+ * const { HttpClient } = require('./client');
+ * const client = new HttpClient();
+ * const res = await client.get('http://localhost:3000/auth', {
+ * headers: { 'content-type':'application/json' },
+ * // body: ..., // .post/.put/.patch only
+ * });
+ * if (!res.ok) {
+ * const txt = await res.text();
+ * return txt;
+ * }
+ * if (!await res.isJson()) {
+ * const txt = await res.text();
+ * return txt;
+ * }
+ * const obj = await res.json();
+ * // ... whatever with obj ...
+ */
+
+const { request } = require("http");
+const { URL } = require("url");
+
+class HttpResponse {
+ /**
+ * Wrap an IncomingMessage.
+ * @param {import('http').IncomingMessage} incoming
+ */
+ constructor(incoming) {
+ this._incoming = incoming;
+ this._buf = null;
+ this.statusCode = incoming.statusCode || 0;
+ this.statusText = incoming.statusMessage || "";
+ this.ok = this.statusCode >= 200 && this.statusCode < 300;
+ this.headers = incoming.headers;
+ this.header = incoming.header;
+ this._json = null;
+ }
+
+ /**
+ * Buffer the full response body (private).
+ * @returns {Promise<Buffer>}
+ */
+ async #readAll() {
+ return new Promise((resolve, reject) => {
+ const bufs = [];
+ this._incoming.on("data", (b) => bufs.push(b));
+ this._incoming.on("end", () => resolve(Buffer.concat(bufs)));
+ this._incoming.on("error", reject);
+ });
+ }
+
+ /**
+ * Return the response body as a string. Buffers on first call and reuses the value.
+ * @returns {Promise<string>}
+ */
+ async text() {
+ if (!this._buf) this._buf = await this.#readAll();
+ return this._buf.toString();
+ }
+
+ /**
+ * Feature-detect whether the body is valid JSON.
+ * Uses text() as the single source-of-truth for the buffered body.
+ * @returns {Promise<boolean>}
+ */
+ async isJson() {
+ try {
+ this._json = JSON.parse(await this.text());
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
+ /**
+ * Parse and return JSON body. Throws if body is not JSON.
+ * @returns {Promise<any>}
+ */
+ async json() {
+ if ((await this.isJson()) === false) throw new Error("not json");
+ if (this._json == null) this._json = JSON.parse(await this.text());
+ return this._json;
+ }
+}
+
+class HttpClient {
+ /**
+ * Internal request factory that performs a request and returns HttpResponse.
+ * Private by design: use convenience methods (get/post/...) instead.
+ *
+ * @param {string} method HTTP method
+ * @param {string} url Request URL
+ * @param {{ headers?: Record<string,string>, body?: string|Buffer|any }} [config]
+ * @returns {Promise<HttpResponse>}
+ */
+ async #requestFactory(method, url, config = {}) {
+ const u = new URL(url);
+ const req = request({
+ method,
+ protocol: u.protocol,
+ hostname: u.hostname,
+ port: u.port,
+ path: u.pathname + u.search,
+ headers: config.headers || {},
+ });
+
+ // write body if provided
+ if (config.body) {
+ if (typeof config.body === "string" || Buffer.isBuffer(config.body)) {
+ req.write(config.body);
+ } else {
+ req.write(JSON.stringify(config.body));
+ }
+ }
+
+ req.end();
+
+ const incoming = await new Promise((resolve, reject) => {
+ req.on("error", reject);
+ req.on("response", resolve);
+ });
+
+ return new HttpResponse(incoming);
+ }
+
+ // Convenience methods for common HTTP verbs:
+
+ /**
+ * GET request
+ * @param {string} url
+ * @param {{ headers?: Record<string,string>, body?: string|Buffer|any }} [config]
+ */
+ get(url, config) {
+ return this.#requestFactory("GET", url, config);
+ }
+
+ /**
+ * POST request
+ */
+ post(url, config) {
+ return this.#requestFactory("POST", url, config);
+ }
+
+ /**
+ * PUT request
+ */
+ put(url, config) {
+ return this.#requestFactory("PUT", url, config);
+ }
+
+ /**
+ * PATCH request
+ */
+ patch(url, config) {
+ return this.#requestFactory("PATCH", url, config);
+ }
+
+ /**
+ * DELETE request
+ */
+ delete(url, config) {
+ return this.#requestFactory("DELETE", url, config);
+ }
+
+ /**
+ * HEAD request
+ */
+ head(url, config) {
+ return this.#requestFactory("HEAD", url, config);
+ }
+
+ /**
+ * OPTIONS request
+ */
+ options(url, config) {
+ return this.#requestFactory("OPTIONS", url, config);
+ }
+
+ /**
+ * Custom method
+ * @param {string} method
+ */
+ custom(method, url, config) {
+ return this.#requestFactory(method, url, config);
+ }
+}
+
+module.exports = { HttpClient, HttpResponse };
data/ssh_command@@ -17,19 +17,21 @@ USERNAME=$1
RES_JSON=$(/usr/bin/ssh_command_node "${USERNAME}")
EXIT=$?
-echo "result => (${EXIT})\n-----------\n\n" >> /home/git/ssh_commands.log
+echo "===> ${RES_JSON}\n" >> /opt/ssh_commands.log
+
+echo "result => (${EXIT})\n-----------\n" >> /opt/ssh_commands.log
COMMAND=$(echo "$RES_JSON" | jq -r '.command')
AUTH_MODE=$(echo "$RES_JSON" | jq -r '.authMode')
GIT_REPO_DIR=$(echo "$RES_JSON" | jq -r '.gitRepositoryDir')
-echo "AUTH_MODE: ${AUTH_MODE}" >> /home/git/ssh_commands.log
-echo "GIT_REPO_DIR: ${GIT_REPO_DIR}" >> /home/git/ssh_commands.log
-echo "ssh_command_node stdout: ${RES_JSON}" >> /home/git/ssh_commands.log
-echo "ssh_command_node exit code: ${EXIT}" >> /home/git/ssh_commands.log
+echo "AUTH_MODE: ${AUTH_MODE}" >> /opt/ssh_commands.log
+echo "GIT_REPO_DIR: ${GIT_REPO_DIR}" >> /opt/ssh_commands.log
+echo "ssh_command_node stdout: ${RES_JSON}" >> /opt/ssh_commands.log
+echo "ssh_command_node exit code: ${EXIT}" >> /opt/ssh_commands.log
if [ "$EXIT" = "0" ]; then
- $COMMAND $GIT_REPO_DIR;
+ LANG=C $COMMAND $GIT_REPO_DIR;
exit $?
else
echo "Forbidden access.\n"
data/ssh_command_node@@ -1,8 +1,27 @@
#!/usr/bin/node
-const http = require("http");
+const { HttpClient } = require("./http_client");
+// const { HttpClient } = require("/home/debian/http_client.js");
const fs = require("fs");
+const LOGS_FILE = "/opt/ssh_commands.log";
+
+function log(message, ...args) {
+ try {
+ fs.appendFileSync(
+ LOGS_FILE,
+ JSON.stringify({
+ timestamp: new Date().toISOString(),
+ message,
+ args,
+ }) + "\n",
+ { encoding: "utf8" },
+ );
+ } catch (err) {
+ // console.log(message, ...args);
+ }
+}
+
async function main(args, sshOriginalCommand) {
const [_, __, username] = args;
...@@ -30,19 +49,22 @@ async function main(args, sshOriginalCommand) {
)
.filter((x) => x != null && x.type === "key");
- console.log("authkeys:", authKeys);
- console.log("username", username);
+ // console.log("authkeys:", authKeys);
+ // console.log("username", username);
- let pk = authKeys.find(
+ let userPk = authKeys.find(
(key) =>
key.text.includes(`command="ssh_command ${username}"`) ||
key.text.includes(`command="/usr/bin/ssh_command ${username}"`),
);
- if (pk) {
- pk = pk.text;
+ if (userPk == null) {
+ log("No key matched ssh connection in authorized_keys file.", { username });
+ return process.exit(128);
}
+ const pk = userPk.text;
+
const sshRsaIndex = pk.indexOf("ssh-rsa");
const publicKey = pk.substring(sshRsaIndex);
...@@ -50,12 +72,6 @@ async function main(args, sshOriginalCommand) {
.split(" ")
.map((part) => part.replace(/\'/g, "").trim());
- fs.appendFileSync(
- "/home/git/ssh_commands.log",
- `username: ${username}\npublicKey: ${publicKey}\ncommand: ${command}\nrepoSlug: ${repoSlug}\n-----------\n`,
- { encoding: "utf8" },
- );
-
const data = JSON.stringify({
command,
repoSlug,
...@@ -63,75 +79,66 @@ async function main(args, sshOriginalCommand) {
publicKey,
});
- const options = {
- hostname: "localhost",
- port: 1337,
- path: "/_ssh/auth",
- method: "POST",
+ log("Will authenticate through /_ssh/auth", {
+ command,
+ repoSlug,
+ username,
+ publicKey,
+ });
+
+ const client = new HttpClient();
+
+ // console.log("HttpClient.instance:", client);
+
+ const res = await client.post("http://localhost:1337/_ssh/auth", {
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(data),
},
- };
-
- let json = {};
-
- const req = http.request(options, (res) => {
- let chunks = [];
-
- res.on("data", (chunk) => {
- chunks.push(chunk);
- });
-
- res.on("end", () => {
- const responseBody = Buffer.concat(chunks).toString();
-
- if (res.statusCode >= 400) {
- // Log error details
- fs.appendFileSync(
- "/home/git/ssh_commands.log",
- `${res.statusCode}: ${res.statusMessage} - ${responseBody}\n-----------\n`,
- { encoding: "utf8" },
- );
- process.exit(128);
- } else {
- // Parse JSON response
- try {
- json = JSON.parse(responseBody);
- // Do something with json if needed
- } catch (e) {
- // handle JSON parse error if necessary
- }
- }
- });
+ body: data,
});
- req.on("error", (e) => {
- // handle request error
- fs.appendFileSync(
- "/home/git/ssh_commands.log",
- `Request error: ${e.message}\n-----------\n`,
- { encoding: "utf8" },
+ // console.log("res:", res);
+
+ if (res.ok === false) {
+ log(
+ "/_ssh/auth response is an error:",
+ res.statusCode,
+ res.statusText,
+ await res.text(),
);
- process.exit(128);
- });
+ return process.exit(128);
+ }
- // Write data to request body
- req.write(data);
- req.end();
+ if ((await res.isJson()) === false) {
+ log(
+ "/_ssh/auth response is not json:",
+ res.statusCode,
+ res.statusText,
+ await res.text(),
+ );
+ return process.exit(128);
+ }
- // fs.appendFileSync(
- // "/home/git/ssh_commands.log",
- // `${res.status}: ${res.statusText} - ${JSON.stringify(json)}\n-----------\n`,
- // { encoding: "utf8" },
- // );
+ const json = await res.json();
+
+ log("/_ssh/auth response json:", res.statusCode, res.statusText, json);
if (json.success === false) {
- process.exit(128);
+ log(
+ "/_ssh/auth response is not successful:",
+ res.statusCode,
+ res.statusText,
+ json,
+ );
+ return process.exit(128);
}
- // success!
+ json.gitRepositoryDir = `/home/debian/data/gitfoss_repos/${repoSlug.replace(/\.git$/, "")}`;
console.log(JSON.stringify(json));
+
+ // success!
+ log("/_ssh/auth response success!", json);
process.exit(0);
}
docker-compose.caddy.yml@@ -44,6 +44,9 @@ services:
- reverse-proxy-public
volumes:
- ./data/gitfoss_repos:/var/lib/gitfoss/repos
+ - ./data/ssh_commands.log:/opt/ssh_commands.log
+ - /usr/bin/ssh_command:/usr/bin/ssh_command
+ - /usr/bin/ssh_command_node:/usr/bin/ssh_command_node
#- /home/git/repos:/var/lib/gitfoss/repos
networks:
scripts/docker-build-scp-deploy.sh@@ -100,6 +100,6 @@ build_and_deploy \
${DOCKER_IMAGE_NAME:-gitfoss_web} \
${DOCKER_IMAGE_TAG:-latest} \
${REMOTE_SSH_USER:-debian} \
- ${REMOTE_SSH_HOST:-92.243.16.118} \
- ${REMOTE_SSH_PORT:-22} \
+ ${REMOTE_SSH_HOST:-gitfoss.dev} \
+ ${REMOTE_SSH_PORT:-1338} \
${REMOTE_DESTINATION_PATH:-/home/debian/}
todo.md@@ -1,14 +1,14 @@
todo:
-- [ ] make ssh server work every times
+- [ ] make ssh server work every times !!!!!
- [x] make the islands runtime load dependencies properly
- [ ] finish merge pull request feature
- [x] add ssh key feature
- [ ] add update/delete ssh key feature
- [ ] add a function to change default branch -> update repo/HEAD ref, update config->init.defaultBranch
- [ ] add a function to enable/disable fast forward push
-- [ ] (?) allow to write comments on a PR
+- [ ] (?) allow to write comments on a PR
- [ ] update repo/description file when repo name or description is set (create, update)
-- [ ] add a select button to change branch/tag (repository browser)
+- [x] add a select button to change branch/tag (repository browser)
- [ ] make it possible to edit user profile (avatar, display name, email, password, etc)
- [ ] make it possible to change app theme (colors) via config file
@@ -17,19 +17,21 @@ USERNAME=$1
RES_JSON=$(/usr/bin/ssh_command_node "${USERNAME}")
EXIT=$?
-echo "result => (${EXIT})\n-----------\n\n" >> /home/git/ssh_commands.log
+echo "===> ${RES_JSON}\n" >> /opt/ssh_commands.log
+
+echo "result => (${EXIT})\n-----------\n" >> /opt/ssh_commands.log
COMMAND=$(echo "$RES_JSON" | jq -r '.command')
AUTH_MODE=$(echo "$RES_JSON" | jq -r '.authMode')
GIT_REPO_DIR=$(echo "$RES_JSON" | jq -r '.gitRepositoryDir')
-echo "AUTH_MODE: ${AUTH_MODE}" >> /home/git/ssh_commands.log
-echo "GIT_REPO_DIR: ${GIT_REPO_DIR}" >> /home/git/ssh_commands.log
-echo "ssh_command_node stdout: ${RES_JSON}" >> /home/git/ssh_commands.log
-echo "ssh_command_node exit code: ${EXIT}" >> /home/git/ssh_commands.log
+echo "AUTH_MODE: ${AUTH_MODE}" >> /opt/ssh_commands.log
+echo "GIT_REPO_DIR: ${GIT_REPO_DIR}" >> /opt/ssh_commands.log
+echo "ssh_command_node stdout: ${RES_JSON}" >> /opt/ssh_commands.log
+echo "ssh_command_node exit code: ${EXIT}" >> /opt/ssh_commands.log
if [ "$EXIT" = "0" ]; then
- $COMMAND $GIT_REPO_DIR;
+ LANG=C $COMMAND $GIT_REPO_DIR;
exit $?
else
echo "Forbidden access.\n"
@@ -1,8 +1,27 @@
#!/usr/bin/node
-const http = require("http");
+const { HttpClient } = require("./http_client");
+// const { HttpClient } = require("/home/debian/http_client.js");
const fs = require("fs");
+const LOGS_FILE = "/opt/ssh_commands.log";
+
+function log(message, ...args) {
+ try {
+ fs.appendFileSync(
+ LOGS_FILE,
+ JSON.stringify({
+ timestamp: new Date().toISOString(),
+ message,
+ args,
+ }) + "\n",
+ { encoding: "utf8" },
+ );
+ } catch (err) {
+ // console.log(message, ...args);
+ }
+}
+
async function main(args, sshOriginalCommand) {
const [_, __, username] = args;
@@ -30,19 +49,22 @@ async function main(args, sshOriginalCommand) {
)
.filter((x) => x != null && x.type === "key");
- console.log("authkeys:", authKeys);
- console.log("username", username);
+ // console.log("authkeys:", authKeys);
+ // console.log("username", username);
- let pk = authKeys.find(
+ let userPk = authKeys.find(
(key) =>
key.text.includes(`command="ssh_command ${username}"`) ||
key.text.includes(`command="/usr/bin/ssh_command ${username}"`),
);
- if (pk) {
- pk = pk.text;
+ if (userPk == null) {
+ log("No key matched ssh connection in authorized_keys file.", { username });
+ return process.exit(128);
}
+ const pk = userPk.text;
+
const sshRsaIndex = pk.indexOf("ssh-rsa");
const publicKey = pk.substring(sshRsaIndex);
@@ -50,12 +72,6 @@ async function main(args, sshOriginalCommand) {
.split(" ")
.map((part) => part.replace(/\'/g, "").trim());
- fs.appendFileSync(
- "/home/git/ssh_commands.log",
- `username: ${username}\npublicKey: ${publicKey}\ncommand: ${command}\nrepoSlug: ${repoSlug}\n-----------\n`,
- { encoding: "utf8" },
- );
-
const data = JSON.stringify({
command,
repoSlug,
@@ -63,75 +79,66 @@ async function main(args, sshOriginalCommand) {
publicKey,
});
- const options = {
- hostname: "localhost",
- port: 1337,
- path: "/_ssh/auth",
- method: "POST",
+ log("Will authenticate through /_ssh/auth", {
+ command,
+ repoSlug,
+ username,
+ publicKey,
+ });
+
+ const client = new HttpClient();
+
+ // console.log("HttpClient.instance:", client);
+
+ const res = await client.post("http://localhost:1337/_ssh/auth", {
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(data),
},
- };
-
- let json = {};
-
- const req = http.request(options, (res) => {
- let chunks = [];
-
- res.on("data", (chunk) => {
- chunks.push(chunk);
- });
-
- res.on("end", () => {
- const responseBody = Buffer.concat(chunks).toString();
-
- if (res.statusCode >= 400) {
- // Log error details
- fs.appendFileSync(
- "/home/git/ssh_commands.log",
- `${res.statusCode}: ${res.statusMessage} - ${responseBody}\n-----------\n`,
- { encoding: "utf8" },
- );
- process.exit(128);
- } else {
- // Parse JSON response
- try {
- json = JSON.parse(responseBody);
- // Do something with json if needed
- } catch (e) {
- // handle JSON parse error if necessary
- }
- }
- });
+ body: data,
});
- req.on("error", (e) => {
- // handle request error
- fs.appendFileSync(
- "/home/git/ssh_commands.log",
- `Request error: ${e.message}\n-----------\n`,
- { encoding: "utf8" },
+ // console.log("res:", res);
+
+ if (res.ok === false) {
+ log(
+ "/_ssh/auth response is an error:",
+ res.statusCode,
+ res.statusText,
+ await res.text(),
);
- process.exit(128);
- });
+ return process.exit(128);
+ }
- // Write data to request body
- req.write(data);
- req.end();
+ if ((await res.isJson()) === false) {
+ log(
+ "/_ssh/auth response is not json:",
+ res.statusCode,
+ res.statusText,
+ await res.text(),
+ );
+ return process.exit(128);
+ }
- // fs.appendFileSync(
- // "/home/git/ssh_commands.log",
- // `${res.status}: ${res.statusText} - ${JSON.stringify(json)}\n-----------\n`,
- // { encoding: "utf8" },
- // );
+ const json = await res.json();
+
+ log("/_ssh/auth response json:", res.statusCode, res.statusText, json);
if (json.success === false) {
- process.exit(128);
+ log(
+ "/_ssh/auth response is not successful:",
+ res.statusCode,
+ res.statusText,
+ json,
+ );
+ return process.exit(128);
}
- // success!
+ json.gitRepositoryDir = `/home/debian/data/gitfoss_repos/${repoSlug.replace(/\.git$/, "")}`;
console.log(JSON.stringify(json));
+
+ // success!
+ log("/_ssh/auth response success!", json);
process.exit(0);
}
@@ -44,6 +44,9 @@ services:
- reverse-proxy-public
volumes:
- ./data/gitfoss_repos:/var/lib/gitfoss/repos
+ - ./data/ssh_commands.log:/opt/ssh_commands.log
+ - /usr/bin/ssh_command:/usr/bin/ssh_command
+ - /usr/bin/ssh_command_node:/usr/bin/ssh_command_node
#- /home/git/repos:/var/lib/gitfoss/repos
networks:
@@ -100,6 +100,6 @@ build_and_deploy \
${DOCKER_IMAGE_NAME:-gitfoss_web} \
${DOCKER_IMAGE_TAG:-latest} \
${REMOTE_SSH_USER:-debian} \
- ${REMOTE_SSH_HOST:-92.243.16.118} \
- ${REMOTE_SSH_PORT:-22} \
+ ${REMOTE_SSH_HOST:-gitfoss.dev} \
+ ${REMOTE_SSH_PORT:-1338} \
${REMOTE_DESTINATION_PATH:-/home/debian/}
@@ -1,14 +1,14 @@
todo:
-- [ ] make ssh server work every times
+- [ ] make ssh server work every times !!!!!
- [x] make the islands runtime load dependencies properly
- [ ] finish merge pull request feature
- [x] add ssh key feature
- [ ] add update/delete ssh key feature
- [ ] add a function to change default branch -> update repo/HEAD ref, update config->init.defaultBranch
- [ ] add a function to enable/disable fast forward push
-- [ ] (?) allow to write comments on a PR
+- [ ] (?) allow to write comments on a PR
- [ ] update repo/description file when repo name or description is set (create, update)
-- [ ] add a select button to change branch/tag (repository browser)
+- [x] add a select button to change branch/tag (repository browser)
- [ ] make it possible to edit user profile (avatar, display name, email, password, etc)
- [ ] make it possible to change app theme (colors) via config file
@@ -17,19 +17,21 @@ USERNAME=$1
RES_JSON=$(/usr/bin/ssh_command_node "${USERNAME}")
EXIT=$?
-echo "result => (${EXIT})\n-----------\n\n" >> /home/git/ssh_commands.log
+echo "===> ${RES_JSON}\n" >> /opt/ssh_commands.log
+
+echo "result => (${EXIT})\n-----------\n" >> /opt/ssh_commands.log
COMMAND=$(echo "$RES_JSON" | jq -r '.command')
AUTH_MODE=$(echo "$RES_JSON" | jq -r '.authMode')
GIT_REPO_DIR=$(echo "$RES_JSON" | jq -r '.gitRepositoryDir')
-echo "AUTH_MODE: ${AUTH_MODE}" >> /home/git/ssh_commands.log
-echo "GIT_REPO_DIR: ${GIT_REPO_DIR}" >> /home/git/ssh_commands.log
-echo "ssh_command_node stdout: ${RES_JSON}" >> /home/git/ssh_commands.log
-echo "ssh_command_node exit code: ${EXIT}" >> /home/git/ssh_commands.log
+echo "AUTH_MODE: ${AUTH_MODE}" >> /opt/ssh_commands.log
+echo "GIT_REPO_DIR: ${GIT_REPO_DIR}" >> /opt/ssh_commands.log
+echo "ssh_command_node stdout: ${RES_JSON}" >> /opt/ssh_commands.log
+echo "ssh_command_node exit code: ${EXIT}" >> /opt/ssh_commands.log
if [ "$EXIT" = "0" ]; then
- $COMMAND $GIT_REPO_DIR;
+ LANG=C $COMMAND $GIT_REPO_DIR;
exit $?
else
echo "Forbidden access.\n"
@@ -1,8 +1,27 @@
#!/usr/bin/node
-const http = require("http");
+const { HttpClient } = require("./http_client");
+// const { HttpClient } = require("/home/debian/http_client.js");
const fs = require("fs");
+const LOGS_FILE = "/opt/ssh_commands.log";
+
+function log(message, ...args) {
+ try {
+ fs.appendFileSync(
+ LOGS_FILE,
+ JSON.stringify({
+ timestamp: new Date().toISOString(),
+ message,
+ args,
+ }) + "\n",
+ { encoding: "utf8" },
+ );
+ } catch (err) {
+ // console.log(message, ...args);
+ }
+}
+
async function main(args, sshOriginalCommand) {
const [_, __, username] = args;
@@ -30,19 +49,22 @@ async function main(args, sshOriginalCommand) {
)
.filter((x) => x != null && x.type === "key");
- console.log("authkeys:", authKeys);
- console.log("username", username);
+ // console.log("authkeys:", authKeys);
+ // console.log("username", username);
- let pk = authKeys.find(
+ let userPk = authKeys.find(
(key) =>
key.text.includes(`command="ssh_command ${username}"`) ||
key.text.includes(`command="/usr/bin/ssh_command ${username}"`),
);
- if (pk) {
- pk = pk.text;
+ if (userPk == null) {
+ log("No key matched ssh connection in authorized_keys file.", { username });
+ return process.exit(128);
}
+ const pk = userPk.text;
+
const sshRsaIndex = pk.indexOf("ssh-rsa");
const publicKey = pk.substring(sshRsaIndex);
@@ -50,12 +72,6 @@ async function main(args, sshOriginalCommand) {
.split(" ")
.map((part) => part.replace(/\'/g, "").trim());
- fs.appendFileSync(
- "/home/git/ssh_commands.log",
- `username: ${username}\npublicKey: ${publicKey}\ncommand: ${command}\nrepoSlug: ${repoSlug}\n-----------\n`,
- { encoding: "utf8" },
- );
-
const data = JSON.stringify({
command,
repoSlug,
@@ -63,75 +79,66 @@ async function main(args, sshOriginalCommand) {
publicKey,
});
- const options = {
- hostname: "localhost",
- port: 1337,
- path: "/_ssh/auth",
- method: "POST",
+ log("Will authenticate through /_ssh/auth", {
+ command,
+ repoSlug,
+ username,
+ publicKey,
+ });
+
+ const client = new HttpClient();
+
+ // console.log("HttpClient.instance:", client);
+
+ const res = await client.post("http://localhost:1337/_ssh/auth", {
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(data),
},
- };
-
- let json = {};
-
- const req = http.request(options, (res) => {
- let chunks = [];
-
- res.on("data", (chunk) => {
- chunks.push(chunk);
- });
-
- res.on("end", () => {
- const responseBody = Buffer.concat(chunks).toString();
-
- if (res.statusCode >= 400) {
- // Log error details
- fs.appendFileSync(
- "/home/git/ssh_commands.log",
- `${res.statusCode}: ${res.statusMessage} - ${responseBody}\n-----------\n`,
- { encoding: "utf8" },
- );
- process.exit(128);
- } else {
- // Parse JSON response
- try {
- json = JSON.parse(responseBody);
- // Do something with json if needed
- } catch (e) {
- // handle JSON parse error if necessary
- }
- }
- });
+ body: data,
});
- req.on("error", (e) => {
- // handle request error
- fs.appendFileSync(
- "/home/git/ssh_commands.log",
- `Request error: ${e.message}\n-----------\n`,
- { encoding: "utf8" },
+ // console.log("res:", res);
+
+ if (res.ok === false) {
+ log(
+ "/_ssh/auth response is an error:",
+ res.statusCode,
+ res.statusText,
+ await res.text(),
);
- process.exit(128);
- });
+ return process.exit(128);
+ }
- // Write data to request body
- req.write(data);
- req.end();
+ if ((await res.isJson()) === false) {
+ log(
+ "/_ssh/auth response is not json:",
+ res.statusCode,
+ res.statusText,
+ await res.text(),
+ );
+ return process.exit(128);
+ }
- // fs.appendFileSync(
- // "/home/git/ssh_commands.log",
- // `${res.status}: ${res.statusText} - ${JSON.stringify(json)}\n-----------\n`,
- // { encoding: "utf8" },
- // );
+ const json = await res.json();
+
+ log("/_ssh/auth response json:", res.statusCode, res.statusText, json);
if (json.success === false) {
- process.exit(128);
+ log(
+ "/_ssh/auth response is not successful:",
+ res.statusCode,
+ res.statusText,
+ json,
+ );
+ return process.exit(128);
}
- // success!
+ json.gitRepositoryDir = `/home/debian/data/gitfoss_repos/${repoSlug.replace(/\.git$/, "")}`;
console.log(JSON.stringify(json));
+
+ // success!
+ log("/_ssh/auth response success!", json);
process.exit(0);
}
@@ -44,6 +44,9 @@ services:
- reverse-proxy-public
volumes:
- ./data/gitfoss_repos:/var/lib/gitfoss/repos
+ - ./data/ssh_commands.log:/opt/ssh_commands.log
+ - /usr/bin/ssh_command:/usr/bin/ssh_command
+ - /usr/bin/ssh_command_node:/usr/bin/ssh_command_node
#- /home/git/repos:/var/lib/gitfoss/repos
networks:
@@ -100,6 +100,6 @@ build_and_deploy \
${DOCKER_IMAGE_NAME:-gitfoss_web} \
${DOCKER_IMAGE_TAG:-latest} \
${REMOTE_SSH_USER:-debian} \
- ${REMOTE_SSH_HOST:-92.243.16.118} \
- ${REMOTE_SSH_PORT:-22} \
+ ${REMOTE_SSH_HOST:-gitfoss.dev} \
+ ${REMOTE_SSH_PORT:-1338} \
${REMOTE_DESTINATION_PATH:-/home/debian/}
@@ -1,14 +1,14 @@
todo:
-- [ ] make ssh server work every times
+- [ ] make ssh server work every times !!!!!
- [x] make the islands runtime load dependencies properly
- [ ] finish merge pull request feature
- [x] add ssh key feature
- [ ] add update/delete ssh key feature
- [ ] add a function to change default branch -> update repo/HEAD ref, update config->init.defaultBranch
- [ ] add a function to enable/disable fast forward push
-- [ ] (?) allow to write comments on a PR
+- [ ] (?) allow to write comments on a PR
- [ ] update repo/description file when repo name or description is set (create, update)
-- [ ] add a select button to change branch/tag (repository browser)
+- [x] add a select button to change branch/tag (repository browser)
- [ ] make it possible to edit user profile (avatar, display name, email, password, etc)
- [ ] make it possible to change app theme (colors) via config file