feat: add minio proxy
This commit is contained in:
		@@ -62,7 +62,15 @@ export class UserApp {
 | 
			
		||||
    const user = this.user;
 | 
			
		||||
    const key = 'user:app:exist:' + app + ':' + user;
 | 
			
		||||
    const value = await redis.get(key);
 | 
			
		||||
    return value;
 | 
			
		||||
    if (!value) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    const [indexFilePath, etag, proxy] = value.split('||');
 | 
			
		||||
    return {
 | 
			
		||||
      indexFilePath,
 | 
			
		||||
      etag,
 | 
			
		||||
      proxy: proxy === 'true',
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * 获取缓存数据,不存在不会加载
 | 
			
		||||
@@ -83,6 +91,8 @@ export class UserApp {
 | 
			
		||||
    const user = this.user;
 | 
			
		||||
    const key = 'user:app:set:' + app + ':' + user;
 | 
			
		||||
    const value = await redis.hget(key, appFileUrl);
 | 
			
		||||
    // const values = await redis.hgetall(key);
 | 
			
		||||
    // console.log('getFile', values);
 | 
			
		||||
    return value;
 | 
			
		||||
  }
 | 
			
		||||
  static async getDomainApp(domain: string) {
 | 
			
		||||
@@ -170,7 +180,28 @@ export class UserApp {
 | 
			
		||||
      // return false;
 | 
			
		||||
      fetchData.type = 'oss';
 | 
			
		||||
    }
 | 
			
		||||
    console.log('fetchData', JSON.stringify(fetchData.data.files, null, 2));
 | 
			
		||||
 | 
			
		||||
    this.setLoaded('loading', 'loading');
 | 
			
		||||
    const loadProxy = async () => {
 | 
			
		||||
      const value = fetchData;
 | 
			
		||||
      await redis.set(key, JSON.stringify(value));
 | 
			
		||||
      const version = value.version;
 | 
			
		||||
      let indexHtml = resources + '/' + user + '/' + app + '/' + version + '/index.html';
 | 
			
		||||
      const files = value?.data?.files || [];
 | 
			
		||||
      const data = {};
 | 
			
		||||
 | 
			
		||||
      // 将文件名和路径添加到 `data` 对象中
 | 
			
		||||
      files.forEach((file) => {
 | 
			
		||||
        if (file.name === 'index.html') {
 | 
			
		||||
          indexHtml = resources + '/' + file.path;
 | 
			
		||||
        }
 | 
			
		||||
        data[file.name] = resources + '/' + file.path;
 | 
			
		||||
      });
 | 
			
		||||
      await redis.set('user:app:exist:' + app + ':' + user, indexHtml + '||etag||true', 'EX', 60 * 60 * 24 * 7); // 7天
 | 
			
		||||
      await redis.hset('user:app:set:' + app + ':' + user, data);
 | 
			
		||||
      this.setLoaded('running', 'loaded');
 | 
			
		||||
    };
 | 
			
		||||
    const loadFilesFn = async () => {
 | 
			
		||||
      const value = await downloadUserAppFiles(user, app, fetchData);
 | 
			
		||||
      if (value.data.files.length === 0) {
 | 
			
		||||
@@ -191,15 +222,8 @@ export class UserApp {
 | 
			
		||||
          encoding: 'utf-8',
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      let valueIndexHtml = value.data.files.find((file) => file.name === 'index.html');
 | 
			
		||||
      if (!valueIndexHtml) {
 | 
			
		||||
        valueIndexHtml = value.data.files.find((file) => file.name === 'index.js');
 | 
			
		||||
        if (!valueIndexHtml) {
 | 
			
		||||
          valueIndexHtml = value.data.files[0];
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      await redis.set(key, JSON.stringify(value));
 | 
			
		||||
      await redis.set('user:app:exist:' + app + ':' + user, valueIndexHtml.path, 'EX', 60 * 60 * 24 * 7); // 7天
 | 
			
		||||
      await redis.set('user:app:exist:' + app + ':' + user, 'index.html||etag||false', 'EX', 60 * 60 * 24 * 7); // 7天
 | 
			
		||||
      const files = value.data.files;
 | 
			
		||||
      const data = {};
 | 
			
		||||
 | 
			
		||||
@@ -211,7 +235,15 @@ export class UserApp {
 | 
			
		||||
      this.setLoaded('running', 'loaded');
 | 
			
		||||
    };
 | 
			
		||||
    try {
 | 
			
		||||
      loadFilesFn();
 | 
			
		||||
      if (fetchData.proxy === true) {
 | 
			
		||||
        await loadProxy();
 | 
			
		||||
        return {
 | 
			
		||||
          code: 200,
 | 
			
		||||
          data: 'loaded',
 | 
			
		||||
        };
 | 
			
		||||
      } else {
 | 
			
		||||
        loadFilesFn();
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.error('loadFilesFn error', e);
 | 
			
		||||
      this.setLoaded('error', 'loadFilesFn error');
 | 
			
		||||
@@ -293,11 +325,15 @@ export const downloadUserAppFiles = async (user: string, app: string, data: type
 | 
			
		||||
  }
 | 
			
		||||
  if (data.type === 'oss') {
 | 
			
		||||
    let serverPath = new URL(resources).href + '/';
 | 
			
		||||
    let hasIndexHtml = false;
 | 
			
		||||
    // server download file
 | 
			
		||||
    for (let i = 0; i < files.length; i++) {
 | 
			
		||||
      const file = files[i];
 | 
			
		||||
      const destFile = path.join(uploadFiles, file.name);
 | 
			
		||||
      const destDir = path.dirname(destFile); // 获取目标文件所在的目录路径
 | 
			
		||||
      if (file.name === 'index.html') {
 | 
			
		||||
        hasIndexHtml = true;
 | 
			
		||||
      }
 | 
			
		||||
      // 检查目录是否存在,如果不存在则创建
 | 
			
		||||
      if (!checkFileExistsSync(destDir)) {
 | 
			
		||||
        fs.mkdirSync(destDir, { recursive: true }); // 递归创建目录
 | 
			
		||||
@@ -310,6 +346,15 @@ export const downloadUserAppFiles = async (user: string, app: string, data: type
 | 
			
		||||
        path: destFile.replace(fileStore, '') + '||' + etag,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    if (!hasIndexHtml) {
 | 
			
		||||
      newFiles.push({
 | 
			
		||||
        name: 'index.html',
 | 
			
		||||
        path: path.join(uploadFiles, 'index.html'),
 | 
			
		||||
      });
 | 
			
		||||
      fs.writeFileSync(path.join(uploadFiles, 'index.html'), JSON.stringify(files), {
 | 
			
		||||
        encoding: 'utf-8',
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import { getDNS, isLocalhost } from '@/utils/dns.ts';
 | 
			
		||||
import http from 'http';
 | 
			
		||||
import https from 'https';
 | 
			
		||||
import { UserApp } from './get-user-app.ts';
 | 
			
		||||
import { config, fileStore } from '../module/config.ts';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
@@ -25,7 +26,6 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
 | 
			
		||||
    // 已经代理过了
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  console.log('req', req.url, 'len', config?.apiList?.length);
 | 
			
		||||
  const proxyApiList = config?.apiList || [];
 | 
			
		||||
  const proxyApi = proxyApiList.find((item) => req.url.startsWith(item.path));
 | 
			
		||||
  if (proxyApi && proxyApi?.type === 'static') {
 | 
			
		||||
@@ -33,7 +33,6 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
 | 
			
		||||
  }
 | 
			
		||||
  if (proxyApi) {
 | 
			
		||||
    const _u = new URL(req.url, `${proxyApi.target}`);
 | 
			
		||||
    console.log('proxyApi', req.url, _u.href);
 | 
			
		||||
    // 设置代理请求的目标 URL 和请求头
 | 
			
		||||
    let header: any = {};
 | 
			
		||||
    if (req.headers?.['Authorization']) {
 | 
			
		||||
@@ -160,30 +159,40 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
 | 
			
		||||
 | 
			
		||||
  const userApp = new UserApp({ user, app });
 | 
			
		||||
  let isExist = await userApp.getExist();
 | 
			
		||||
  const createRefreshPage = () => {
 | 
			
		||||
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
 | 
			
		||||
    res.end(createRefreshHtml(user, app));
 | 
			
		||||
  };
 | 
			
		||||
  const createErrorPage = () => {
 | 
			
		||||
    res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
 | 
			
		||||
    res.write('Server Error\n');
 | 
			
		||||
    res.end();
 | 
			
		||||
  };
 | 
			
		||||
  const createNotFoundPage = (msg?: string) => {
 | 
			
		||||
    res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
 | 
			
		||||
    res.write(msg || 'Not Found App\n');
 | 
			
		||||
    res.end();
 | 
			
		||||
  };
 | 
			
		||||
  if (!isExist) {
 | 
			
		||||
    try {
 | 
			
		||||
      const { code, loading } = await userApp.setCacheData();
 | 
			
		||||
      if (loading || code === 20000) {
 | 
			
		||||
        res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
 | 
			
		||||
 | 
			
		||||
        res.end(createRefreshHtml(user, app));
 | 
			
		||||
        return;
 | 
			
		||||
        return createRefreshPage();
 | 
			
		||||
      } else if (code !== 200) {
 | 
			
		||||
        return createErrorPage();
 | 
			
		||||
      }
 | 
			
		||||
      res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
 | 
			
		||||
      res.write('Not Found App\n');
 | 
			
		||||
      res.end();
 | 
			
		||||
      // 不存在就一定先返回loading状态。
 | 
			
		||||
      return;
 | 
			
		||||
      isExist = await userApp.getExist();
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('setCacheData error', error);
 | 
			
		||||
      res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
 | 
			
		||||
      res.write('Server Error\n');
 | 
			
		||||
      res.end();
 | 
			
		||||
      createErrorPage();
 | 
			
		||||
      userApp.setLoaded('error', 'setCacheData error');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const indexFile = isExist; // 已经必定存在了
 | 
			
		||||
  if (!isExist) {
 | 
			
		||||
    return createNotFoundPage();
 | 
			
		||||
  }
 | 
			
		||||
  const indexFile = isExist.indexFilePath; // 已经必定存在了
 | 
			
		||||
  try {
 | 
			
		||||
    let appFileUrl: string;
 | 
			
		||||
    if (domainApp) {
 | 
			
		||||
@@ -191,15 +200,47 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
 | 
			
		||||
    } else {
 | 
			
		||||
      appFileUrl = (url + '').replace(`/${user}/${app}/`, '');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const appFile = await userApp.getFile(appFileUrl);
 | 
			
		||||
    if (isExist.proxy) {
 | 
			
		||||
      let proxyUrl = appFile || isExist.indexFilePath;
 | 
			
		||||
      if (!proxyUrl.startsWith('http')) {
 | 
			
		||||
        return createNotFoundPage('Invalid proxy url');
 | 
			
		||||
      }
 | 
			
		||||
      let protocol = proxyUrl.startsWith('https') ? https : http;
 | 
			
		||||
      // 代理
 | 
			
		||||
      const proxyReq = protocol.request(proxyUrl, (proxyRes) => {
 | 
			
		||||
        res.writeHead(proxyRes.statusCode, {
 | 
			
		||||
          ...proxyRes.headers,
 | 
			
		||||
        });
 | 
			
		||||
        if (proxyRes.statusCode === 404) {
 | 
			
		||||
          userApp.clearCacheData();
 | 
			
		||||
          return createNotFoundPage('Invalid proxy url');
 | 
			
		||||
        }
 | 
			
		||||
        if (proxyRes.statusCode === 302) {
 | 
			
		||||
          res.writeHead(302, { Location: proxyRes.headers.location });
 | 
			
		||||
          return res.end();
 | 
			
		||||
        }
 | 
			
		||||
        proxyRes.pipe(res, { end: true });
 | 
			
		||||
      });
 | 
			
		||||
      proxyReq.on('error', (err) => {
 | 
			
		||||
        console.error(`Proxy request error: ${err.message}`);
 | 
			
		||||
        userApp.clearCacheData();
 | 
			
		||||
      });
 | 
			
		||||
      proxyReq.end();
 | 
			
		||||
      // userApp.clearCacheData()
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    console.log('appFile', appFile, appFileUrl);
 | 
			
		||||
    if (!appFile) {
 | 
			
		||||
      const [indexFilePath, etag] = indexFile.split('||');
 | 
			
		||||
      const contentType = getContentType(indexFilePath);
 | 
			
		||||
      const isHTML = contentType.includes('html');
 | 
			
		||||
      const filePath = path.join(fileStore, indexFilePath);
 | 
			
		||||
      if (!userApp.fileCheck(filePath)) {
 | 
			
		||||
        // 动态删除文件
 | 
			
		||||
        res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
 | 
			
		||||
        res.write('File expired, Not Found\n');
 | 
			
		||||
        res.write('App Cache expired, Please refresh\n');
 | 
			
		||||
        res.end();
 | 
			
		||||
        await userApp.clearCacheData();
 | 
			
		||||
        return;
 | 
			
		||||
@@ -255,4 +296,3 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
 | 
			
		||||
    console.error('getFile error', error);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
import { config } from '../config.ts';
 | 
			
		||||
 | 
			
		||||
const api = config?.api || { host: 'kevisual.xiongxiao.me', path: '/api/router' };
 | 
			
		||||
const api = config?.api || { host: 'https://kevisual.xiongxiao.me', path: '/api/router' };
 | 
			
		||||
const apiPath = api.path || '/api/router';
 | 
			
		||||
export const fetchTest = async (id: string) => {
 | 
			
		||||
  const fetchUrl = 'http://' + api.host + apiPath;
 | 
			
		||||
  const fetchUrl = api.host + apiPath;
 | 
			
		||||
  const fetchRes = await fetch(fetchUrl, {
 | 
			
		||||
    method: 'POST',
 | 
			
		||||
    headers: {
 | 
			
		||||
@@ -19,7 +19,7 @@ export const fetchTest = async (id: string) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const fetchDomain = async (domain: string) => {
 | 
			
		||||
  const fetchUrl = 'http://' + api.host + apiPath;
 | 
			
		||||
  const fetchUrl = api.host + apiPath;
 | 
			
		||||
  const fetchRes = await fetch(fetchUrl, {
 | 
			
		||||
    method: 'POST',
 | 
			
		||||
    headers: {
 | 
			
		||||
@@ -37,7 +37,7 @@ export const fetchDomain = async (domain: string) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const fetchApp = async ({ user, app }) => {
 | 
			
		||||
  const fetchUrl = 'http://' + api.host + apiPath;
 | 
			
		||||
  const fetchUrl = api.host + apiPath;
 | 
			
		||||
  const fetchRes = await fetch(fetchUrl, {
 | 
			
		||||
    method: 'POST',
 | 
			
		||||
    headers: {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user