分类目录归档:工具

Ubuntu 桌面应用收集

下载工具:
1 uGet
地址:http://ugetdm.com/downloads-ubuntu

sudo add-apt-repository ppa:plushuang-tw/uget-stable
sudo apt update
sudo apt install uget
# 安装插件
apt install aria2


点击菜单栏的编辑–设置,进入设置选项页面,在设置页面,切换到“插件”标签,勾选“启用 aria2 插件”(aria2是 Linux下一个命令行的下载工具),启用aria2后,设置连接数,鼠标右键点击左侧的默认分类,选择“属性”, 改大链接数量。

输入法:
1 搜狗拼音
下载地址:http://pinyin.sogou.com/linux/?r=pinyin

dpkg -i Download/sogoupinyin_2.1.0.0086_amd64.deb
apt install -f

安装完成后搜索Language Support(语言支持),修改键盘输入法系统为fcitx,重启系统,按shift即可进行中英文切换。

浏览器
1 Chrome
谷歌浏览器下载地址,选择deb包下载(国内可访问):http://www.google.cn/chrome/browser/desktop/index.html

dpkg -i Downloads/google-chrome-stable_current_amd64.deb
apt install -f

VMware虚拟机磁盘压缩备忘

新建的VMware虚拟机,如果不是使用预分配的方式,那么磁盘是动态增长的,这个好处很多,比如可以节约磁盘空间,然后这个磁盘是只增不减的。

VMware提供了压缩的工具。点击虚拟机菜单下的安装VMware Tools(虚拟机必须具备光驱):

挂载光盘,安装工具:

mount /dev/cdrom /mnt
cp VMwareTools-10.1.6-5214329.tar.gz
tar zxvf VMwareTools-10.1.6-5214329.tar.gz
cd vmware-tools-distrib
./vmware-install.pl
open-vm-tools packages are available from the OS vendor and VMware recommends 
using open-vm-tools packages. See http://kb.vmware.com/kb/2073803 for more 
information.
Do you still want to proceed with this installation? [no]  输入yes

一路回车,工具安装完成。

查看磁盘:

/usr/bin/vmware-toolbox-cmd help disk
disk: 执行磁盘压缩操作
用法: /usr/bin/vmware-toolbox-cmd disk <子命令> [参数]

子命令:
   list: 列出可用的位置
   shrink <位置>: 擦除并压缩指定位置的文件系统
   shrinkonly: 压缩所有磁盘
   wipe <位置>: 擦除指定位置的文件系统

/usr/bin/vmware-toolbox-cmd disk list
/
/boot

可以先擦除分区再压缩,也可以同时来:

#擦除:把实际空闲的空间标记为可回收(即可压缩)
/usr/bin/vmware-toolbox-cmd disk wipe /

#压缩
/usr/bin/vmware-toolbox-cmd disk shrinkonly

#擦除并压缩
/usr/bin/vmware-toolbox-cmd disk shrink /

另外,如果需要操作大文件,可以临时挂载一个磁盘,然后在这个磁盘中处理,完成后把该磁盘移除即可。

Mac 开发环境搭建

进入Mac的默认Shell终端,安装Homebrew工具:

Homebrew是一个包管理器,用于在Mac上安装一些OS X没有的UNIX工具(比如著名的wget)。 Homebrew将这些工具统统安装到了/usr/local/Cellar目录并在/usr/local/bin中创建符号链接。

官方网站:
http://brew.sh/index_zh-cn.html

安装:

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

#检查安装:
brew -v

Homebrew 常用命令

brew install git

brew uninstall git

brew search git

brew list

brew update

#更新某具体软件
brew upgrade git

#查看软件信息
brew [info | home] [FORMULA...]

#和upgrade一样,单个软件删除 和 所有程序删除。清理所有已安装软件包的历史老版本
brew cleanup git 
brew cleanup

#查看哪些已安装的程序需要更新
brew outdated

Homebrew 卸载

    cd `brew --prefix`
    rm -rf Cellar
    brew prune 
    rm `git ls-files` 
    rm -rf Library .git .gitignore bin/brew
    rm -rf README.md share/man/man1/brew
    rm -rf Library/Homebrew Library/Aliases 
    rm -rf Library/Formula Library/Contributions
    rm -rf ~/Library/Caches/Homebrew

一 安装PHP

#搜索,会出现几个分之,比如PHP56 PHP71
brew search php
#过滤,只要71分支,提供了非常多扩展包
brew search php71
#安装(选择需要的扩展包)
brew install homebrew/php/php71 homebrew/php/php71-apcu homebrew/php/php71-redis homebrew/php/php71-mongodb homebrew/php/php71-opcache omebrew/php/php71-swoole

大部分PHP的模块,都包含在了homebrew/php/php71中,是编译到内核的(非动态模块),上面的apcu,redis,mogondb,swoole是动态模块,模块安装位置:/usr/local/opt/,比如:/usr/local/opt/php71-apcu/apcu.so。配置文件自然是/usr/local/etc/php/7.1/php.ini,扩展的配置放在/usr/local/etc/php/7.1/conf.d/*.ini。

php -v
php -m

编译到内核的模块确实是大而全,然后还需要调整一下php.ini的配置(才能符合开发环境要求):

#设置时区
date.timezone = Asia/Shanghai
 
#CGI相关参数,实际上建议修改的是force_redirect,其它均保留默认值
cgi.force_redirect = 0   #默认为1,改为0
cgi.fix_pathinfo = 1     #默认是1,保留
fastcgi.impersonate = 1  #默认是1,保留
cgi.rfc2616_headers = 0  #默认是0,保留

#其它参数调整,根据实际情况调整
upload_max_filesize = 64M
max_execution_time = 1200
max_input_time = 600
max_input_nesting_level = 128
max_input_vars = 2048
memory_limit = 1024M
 
post_max_size = 64M

如果要启动PHPFPM,FPM主配置文件/usr/local/etc/php/7.1/php-fpm.conf,池配置在/usr/local/etc/php/7.1/php-fpm.d中,需要注意的是,池配置中,默认的运行用户是和用户组均为_www,所以需要检查文件的权限,保证对_www具有读和执行(默认是符合的),如果要写入,那么还需要保证对应的文件夹有被写入的权限。

启动PHPFPM,由于php-fpm这个命令放入到了/usr/local/sbin中,默认shell并不搜索这个路径,所以要想添加环境变量:

#设置环境变量
export PATH="/usr/local/sbin:$PATH"  
echo 'export PATH="/usr/local/sbin:$PATH"' >> ~/.bash_profile

#确认命令能找到
which php-fpm
which php71-fpm

#手动启动,PHPFPM可以不使用root身份启动(user和group指令无用),会使用当前用户运行
sudo php71-fpm start
sudo php71-fpm stop

对于开发环境,PHPFPM可以不用启动,直接使用PHP内置的HTTP服务器也可以。

二 安装Nginx

brew install --with-http2 nginx  

如果要绑定到80端口,那么Nginx就必须以root身份运行。默认的server配置位于(可改):/usr/local/etc/nginx/servers。可以往里面方式配置:

server {
    listen 80;
    #listen 443 ssl http2;
    server_name test.app;
    root "/Users/xx/www/test/public";
 
    index index.html index.htm index.php;
 
    charset utf-8;
 
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
 
    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }
 
    access_log off;
 
    sendfile off;
 
    location ~ \.php$ {
        client_max_body_size 64M;
        fastcgi_intercept_errors off;
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_read_timeout 300;
        fastcgi_buffer_size 32k;
        fastcgi_buffers 64 32k;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
 
    location ~ /\.ht {
        deny all;
    }
 
    #ssl_certificate     /etc/nginx/ssl/test.app.crt;
    #ssl_certificate_key /etc/nginx/ssl/test.app.key;
}

Nginx的主配置文件设置的启动user一般应该和PHPFPM相同,或者需要保证Nginx对文件具备读和执行的权限。如果是文件上传,还需要确保Nginx对临时中间文件夹具备写入权限。

启动关闭等:

sudo nginx -t
sudo nginx -s start
sudo nginx -s stop

三 安装MySQL

#安装最新版本(5.7.xx)
brew install mysql

#确定搜索路径:
which mysqld
mysqld —verbose —help | grep -A 1 ‘Default options’

/etc/my.cnf  /etc/mysql/my.cnf  /usr/local/etc/my.cnf  ~/.my.cnf

#
mysql.server start
mysql_secure_installation

# 停止
mysql.server stop

MySQL不需要以root身份启动。

四、安装Tomcat

brew search tomcat
==> Searching local taps...
tomcat ✔            tomcat-native       tomcat@6            tomcat@7            tomcat@8
==> Searching taps on GitHub...
==> Searching blacklisted, migrated and deleted formulae...

# 安装最新版本
brew install tomcat

#安装指定版本
brew install tomcat@8

启动关闭:

catalina --help
Using CATALINA_BASE:   /usr/local/Cellar/tomcat/9.0.6/libexec
Using CATALINA_HOME:   /usr/local/Cellar/tomcat/9.0.6/libexec
Using CATALINA_TMPDIR: /usr/local/Cellar/tomcat/9.0.6/libexec/temp
Using JRE_HOME:        /Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home
Using CLASSPATH:       /usr/local/Cellar/tomcat/9.0.6/libexec/bin/bootstrap.jar:/usr/local/Cellar/tomcat/9.0.6/libexec/bin/tomcat-juli.jar
Usage: catalina.sh ( commands ... )
commands:
  debug             Start Catalina in a debugger
  debug -security   Debug Catalina with a security manager
  jpda start        Start Catalina under JPDA debugger
  run               Start Catalina in the current window
  run -security     Start in the current window with security manager
  start             Start Catalina in a separate window
  start -security   Start in a separate window with security manager
  stop              Stop Catalina, waiting up to 5 seconds for the process to end
  stop n            Stop Catalina, waiting up to n seconds for the process to end
  stop -force       Stop Catalina, wait up to 5 seconds and then use kill -KILL if still running
  stop n -force     Stop Catalina, wait up to n seconds and then use kill -KILL if still running
  configtest        Run a basic syntax check on server.xml - check exit code for result
  version           What version of tomcat are you running?
Note: Waiting for the process to end and use of the -force option require that $CATALINA_PID is defined

关于启动问题:
在Mac下,如果要开机启动,可以参考如下配置(一般不需要):

#Nginx
cp /usr/local/opt/nginx/homebrew.mxcl.nginx.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist  

# PHP-FPM
cp /usr/local/opt/php70/homebrew.mxcl.php71.plist ~/Library/LaunchAgents/  
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.php71.plist  

# MySQL
cp /usr/local/opt/mysql/homebrew.mxcl.mysql.plist ~/Library/LaunchAgents/  
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist

## 卸载
launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist  
launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.php71.plist  
launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist  
rm ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist  
rm ~/Library/LaunchAgents/homebrew.mxcl.php71.plist  
rm ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist

更加实际的方法:进入操作系统后,启动Nginx和PHPFPM(因为要sudo,需要输入密码),MySQL则在需要时启动,比如本地一般链接远程数据库。所以可以这个别名:(往.bash_profile中写入)

alias servers.start='sudo nginx && php-fpm --fpm-config /usr/local/etc/php/7.1/php-fpm.conf -D'
alias servers.stop='sudo bash -c "killall -9 php-fpm && nginx -s stop"'                       
alias nginx.logs='tail -f /usr/local/opt/nginx/access.log'
alias nginx.errors='tail -f /usr/local/opt/nginx/error.log'

遇到问题:
1 Nginx启动提示

nginx: [emerg] getgrnam("

提示大体就是找不到用户组的意思。在Nginx配置中,user如果只指定了用户名,默认会去寻找同名的用户组,在Mac中,用户不一定对应一个同名的用户组,所以出现这种情况就是需要明确指定存在的用户组,可以通过如下方式来确定用户和用户组:

#当前登录的用户名
whoami
www

#确认用户组(可见www的uid是502,对应的组id是20,名称是staff)
id
uid=502(www) gid=20(staff) groups=20(staff),12(everyone)

把www和staff对应填入,错误提示消失。

HTML编辑器 – UEditor

UEditor 是一个功能比较齐全的HTML编辑器,百度出品,官方网站为:http://ueditor.baidu.com/website/。这个编辑器对于文件上传方面提供了强大支持,比如可以直接粘贴(CTRL + V)图片,上传涂鸦文件(客户端画图),多文件上传等。

其中有一个叫UMeditor的产品,简称UM,它是UEditor(简称UE)的功能缩减版本。也可以使用其提供的Ubuilder来构建一个自定义的版本(主要指功能模块)。

一般直接使用UEditor这个全功能版本即可:
ueditor

目录结构:
ueditor-struct
目录dialogs对应各种弹框,目录lang对应语言包(前端展示),目录themes对应皮肤,third-party是第三方插件。文件ueditor.all.js是编辑器实现代码,可见未压缩有1M以上,min压缩后也有接近400K,问津ueditor.config.js是前端的主要配置。

前台主要配置都在ueditor.config.js中,这个文件几乎不用改变,一般需要修改的地方就是serverUrl参数,当有图片或文件需要上传时,就提交到这个指定的地址。其它配置项在这个文件中都有详细的注释(比如皮肤,语言等):

(function () {
    var URL = window.UEDITOR_HOME_URL || getUEBasePath();
    var origin = getOrigin();

    /**
     * 配置项主体。注意,此处所有涉及到路径的配置别遗漏URL变量。
     */
    window.UEDITOR_CONFIG = {

        //为编辑器实例添加一个路径,这个不能被注释
        UEDITOR_HOME_URL: URL

        // 服务器统一请求接口路径
        , serverUrl: origin + "/ueditor/server"
    };
    // ...
    // ...
    function getOrigin() {
        if (typeof location.origin === 'undefined') {
            location.origin = location.protocol + '//' + location.host;
        }
        return location.origin;
    }

    window.UE = {
        getUEBasePath: getUEBasePath,
        getOrigin: getOrigin
    };
})();

其中getOrigin()原始文件是没有的,这个用来解决原来的函数获取基本路径不准确的问题。

以上配置指定了/ueditor/serve,那么文件上传时都会POST到这个地址。所以后端需要处理文件上传的逻辑,原本下载包已经提供了相关的实现,不过为了真正可用,需要做一些改造,比如需要验证:

// 抽象类,Upload, 处理文件上传逻辑,有上个子类:UploadCatch.php 、 UploadFile.php 、 UploadScrawl.php
// 分别实现fire()方法,这个方法实际是读取配置,处理上传逻辑
<?php
namespace UEditor;

abstract class Upload
{
    //文件域名
    protected $fileField;
    //文件上传对象
    protected $file;
    //文件上传对象
    protected $base64;
    //配置信息
    protected $config;
    //原始文件名
    protected $oriName;
    //新文件名
    protected $fileName;
    //完整文件名,即从当前配置目录开始的URL
    protected $fullName;
    //完整文件名,即从当前配置目录开始的URL
    protected $filePath;
    //文件大小
    protected $fileSize;
    //文件类型
    protected $fileType;
    //上传状态信息,
    protected $stateInfo;
    //上传状态映射表,国际化用户需考虑此处数据的国际化
    protected $stateMap = array(
        //上传成功标记,在UEditor中内不可改变,否则flash判断会出错
        "SUCCESS",
        "文件大小超出 upload_max_filesize 限制",
        "文件大小超出 MAX_FILE_SIZE 限制",
        "文件未被完整上传",
        "没有文件被上传",
        "上传文件为空",
        "ERROR_TMP_FILE" => "临时文件错误",
        "ERROR_TMP_FILE_NOT_FOUND" => "找不到临时文件",
        "ERROR_SIZE_EXCEED" => "文件大小超出网站限制",
        "ERROR_TYPE_NOT_ALLOWED" => "文件类型不允许",
        "ERROR_CREATE_DIR" => "目录创建失败",
        "ERROR_DIR_NOT_WRITEABLE" => "目录没有写权限",
        "ERROR_FILE_MOVE" => "文件保存时出错",
        "ERROR_FILE_NOT_FOUND" => "找不到上传文件",
        "ERROR_WRITE_CONTENT" => "写入文件内容错误",
        "ERROR_UNKNOWN" => "未知错误",
        "ERROR_DEAD_LINK" => "链接不可用",
        "ERROR_HTTP_LINK" => "链接不是http链接",
        "ERROR_HTTP_CONTENTTYPE" => "链接contentType不正确",
        "INVALID_URL" => "非法 URL",
        "INVALID_IP" => "非法 IP"
    );

    public function __construct(array $config, $request)
    {
        $this->config = $config;
        $this->request = $request;
        $this->fileField = $this->config['fieldName'];
        if (isset($config['allowFiles'])) {
            $this->allowFiles = $config['allowFiles'];
        } else {
            $this->allowFiles = [];
        }
    }

    abstract function fire();

    public function handle()
    {
        $this->fire();
        return $this->getFileInfo();
    }

    /**
     * 上传错误检查
     * @param $errCode
     * @return string
     */
    protected function getStateInfo($errCode)
    {
        return !$this->stateMap[$errCode] ? $this->stateMap["ERROR_UNKNOWN"] : $this->stateMap[$errCode];
    }

    /**
     * 文件大小检测
     * @return bool
     */
    protected function checkSize()
    {
        return $this->fileSize <= ($this->config["maxSize"]);
    }

    /**
     * 获取文件扩展名
     * @return string
     */
    protected function getFileExt()
    {
        return '.' . $this->file->guessExtension();
    }

    /**
     * 重命名文件
     * @return string
     */
    protected function getFullName()
    {
        //替换日期事件
        $t = time();
        $d = explode('-', date("Y-y-m-d-H-i-s"));
        $format = $this->config["pathFormat"];
        $format = str_replace("{yyyy}", $d[0], $format);
        $format = str_replace("{yy}", $d[1], $format);
        $format = str_replace("{mm}", $d[2], $format);
        $format = str_replace("{dd}", $d[3], $format);
        $format = str_replace("{hh}", $d[4], $format);
        $format = str_replace("{ii}", $d[5], $format);
        $format = str_replace("{ss}", $d[6], $format);
        $format = str_replace("{time}", $t, $format);

        //过滤文件名的非法自负,并替换文件名
        $oriName = substr($this->oriName, 0, strrpos($this->oriName, '.'));
        $oriName = preg_replace("/[\|\?\"\<\>\/\*\\\\]+/", '', $oriName);
        $format = str_replace("{filename}", $oriName, $format);

        //替换随机字符串
        $randNum = rand(1, 10000000000) . rand(1, 10000000000);
        if (preg_match("/\{rand\:([\d]*)\}/i", $format, $matches)) {
            $format = preg_replace("/\{rand\:[\d]*\}/i", substr($randNum, 0, $matches[1]), $format);
        }

        $ext = $this->getFileExt();
        return $format . $ext;
    }

    /**
     * 获取文件完整路径
     * @return string
     */
    protected function getFilePath()
    {
        $fullName = $this->fullName;
        $rootPath = public_path();
        $fullName = ltrim($fullName, '/');

        return $rootPath . '/' . $fullName;
    }

    /**
     * 文件类型检测
     * @return bool
     */
    protected function checkType()
    {
        return in_array($this->getFileExt(), $this->config["allowFiles"]);
    }

    /**
     * 获取当前上传成功文件的各项信息
     * @return array
     */
    public function getFileInfo()
    {
        return array(
            "state" => $this->stateInfo,
            "url" => $this->fullName,
            "title" => $this->fileName,
            "original" => $this->oriName,
            "type" => $this->fileType,
            "size" => $this->fileSize
        );
    }
}


// 文件上传
class UploadFile extends Upload
{
    public function fire()
    {
        $file = $this->request->file($this->fileField);
        if (empty($file)) {
            $this->stateInfo = $this->getStateInfo("ERROR_FILE_NOT_FOUND");
            return false;
        }
        if (!$file->isValid()) {
            $this->stateInfo = $this->getStateInfo($file->getError());
            return false;
        }

        $this->file = $file;
        $this->oriName = $this->file->getClientOriginalName();
        $this->fileSize = $this->file->getSize();
        $this->fileType = $this->getFileExt();
        $this->fullName = $this->getFullName();
        $this->filePath = $this->getFilePath();
        $this->fileName = basename($this->filePath);

        //检查文件大小是否超出限制
        if (!$this->checkSize()) {
            $this->stateInfo = $this->getStateInfo("ERROR_SIZE_EXCEED");
            return false;
        }
        //检查是否不允许的文件格式
        if (!$this->checkType()) {
            $this->stateInfo = $this->getStateInfo("ERROR_TYPE_NOT_ALLOWED");
            return false;
        }

        if (config('ueditor.drivers.default') == 'local') {
            try {
                $this->file->move(dirname($this->filePath), $this->fileName);
                $this->stateInfo = $this->stateMap[0];
            } catch (\Symfony\Component\HttpFoundation\File\Exception\FileException $exception) {
                $this->stateInfo = $this->getStateInfo("ERROR_WRITE_CONTENT");
                return false;
            }
        } else {
            $this->stateInfo = $this->getStateInfo("ERROR_UNKNOWN");
            return false;
        }
        return true;
    }
}

要真正可用,还需要结合具体使用的框架做配置,比如在Laravel框架中,经过一些改造,就可以继承到项目中,如下是实际使用部署:
ueditor.zip

至于使用,则非常简单,需要编辑器的地方引入:

<script type="text/javascript" charset="utf-8" src="/ueditor/ueditor.config.js"></script>
<script type="text/javascript" charset="utf-8" src="/ueditor/ueditor.all.min.js"> </script>
<script type="text/javascript" charset="utf-8" src="/ueditor/lang/zh-cn/zh-cn.js"></script>

然后插入类似如下模板:

<div>
    <script id="editor" type="text/plain" style="width:1024px;height:500px;"></script>
</div>

<script type="text/javascript">

    //实例化编辑器
    //建议使用工厂方法getEditor创建和引用编辑器实例,如果在某个闭包下引用该编辑器,直接调用UE.getEditor('editor')就能拿到相关的实例
    var ue = UE.getEditor('editor');
</script>

具体的配置和使用,可以参考:http://fex.baidu.com/ueditor/

使用Vagrant构建本地开发环境

首先安装VirtualBox(https://www.virtualbox.org) 和 Vagrant(https://www.vagrantup.com)。

Vagrant是一个基于Ruby的工具,用于创建和部署虚拟化开发环境。简单来说,Vagrant基于一个虚拟机的(虚拟机运行环境可以是VirtualBox,也可是VMWare),并在它之上,根据自定义配置,构建开发环境,比如:宿主机和虚拟机的文件共享,站点配置,建立数据库等。一般是先安装一个虚拟操作系统,上面安装好必须的应用软件(如Nginx、MySQL、PHP等),然后按照格式打包成一个Box(Vagrant的称呼),Vagrant与VituralBox或VMWare进行交互,根据配置配置把Box还原为一个具体的虚拟机,这些是这个工具主要的工作内容。

操作Vagrant有如下几个步骤:
1 添加Box
Vagrant安装之后,会在~目录中生成.vagrant.d文件夹,里面的boxes是已经添加到Vagrant的box,tmp中是下下载box时的临时文件,如果下载box失败,可以到这里删除临时文件。

#https://atlas.hashicorp.com/laravel/boxes/homestead
vagrant box add laravel/homestead

vagrant-box-search

vagrant-box-laravel-homestead
由于已知原因,这样可以成功的几率很低。所以,我们需要离线下载。比如上面的laravel/homestead跳出的URL是:

https://atlas.hashicorp.com/laravel/boxes/homestead/versions/1.1.0/providers/virtualbox.box

然后用代理下载。下载完毕后得到文件,比如叫homestead-virtualbox.box,由于box是有版本的,而离线下载的情况,box的元数据丢失了,所以需要在homestead-virtualbox.box同类目下建立一个叫metadata.json的文件:

{
    "name": "laravel/homestead",
    "versions": [{
        "version": "1.1.0",
        "providers": [{
            "name": "virtualbox",
            "url": "file://homestead-virtualbox.box"
        }]
    }]
}

然后运行vagrant add metadata.json,这样就跟在线添加box是一样的了。如果直接运行vagrant box add homestead-virtualbox.box,在vagrant中是无法启动的,进入~/.vagrant.d\boxes\laravel-VAGRANTSLASH-homestead查看就很明白,它是根据版本号来存放的。

#查看已经添加的box
vagrant box list

laravel/homestead (virtualbox, 1.1.0)

后面的virtualbox表明这个box是使用VirtualBox来运行的,1.1.0表示这个盒子的版本。

2 在工作目录下生成启动文件
进入到具体的工作目录,运行vagrant init laravel/homestead,就会在当前目录下生成Vagrantfile文件,它需要启动的就是之前添加的laravel/homestead这个box,Vagrantfile是一个简单的Ruby脚本,是很容易看懂的。Vagrantfile文件可以读入一个配置文件,也可以把配置直接全部写入Vagrantfile

3 启动
启动文件Vagrantfile建立好后,在当前工作目录下运行vagrant up就可以启动。这个命令会搜索当前目录下的Vagrantfile,如果找不到则会到~目录寻找Vagrantfile。Box启动时,会在和Vagrantfile相同的目录下生成一个.vagrant目录,里面保存了启动后相关的文件,box消毁后会自动删除相关文件。

如果把所有文件都硬编码到Vagrantfile,明显不是最佳实践,所以一般都会把配置提取到一个外部的配置文件,配置文件格式和位置就完全取决Vagrantfile能不能以及如何处理。

只要找到Vagrantfile,并且指定的box存在,box就可以启动。但是Vagrantfile可以配置的参数实际是很多的,所以最好是在Vagrantfile实现编程,把外部的文件读取进来,然后传递给Vagrant让其启动Box。Laravel为其提供的laravel/homestead盒子对应了一个项目()https://github.com/laravel/homestead),说实在的,这个项目仅仅提供了一段Ruby脚本而已(提供Vagrantfile,核心是scripts/homestead.rb),直接克隆下来:

#也可以直接下在Zip包
git clone https://github.com/laravel/homestead.git Homestead

#然后运行init脚本
init 

这个脚本实际就仅仅拷贝几个文件(作为模子),怪胎就在于,它跑到~目录下生成.homestead目录,拷贝进去的文件是after.sh, aliases, Homestead.yaml。主要就是这个Homestead.yaml文件,在其提供的Vagrantfile的文件中(它实现了编程),会读入这个配置文件,所以只需要修改这个配置文件即可:

---
ip: "192.168.10.10"
memory: 2048
cpus: 1
provider: virtualbox

authorize: ~/.ssh/id_rsa.pub

name: homestead

keys:
    - ~/.ssh/id_rsa

folders:
    - map: E:/var/www
      to: /home/vagrant/www

sites:
    - map: ebt.app
      to: /home/vagrant/www/ebt/public
    - map: elc.app
      to: /home/vagrant/www/elc/public

databases:
    - ebt

更多的参数,参考scripts/homestead.rb就可以很容易提取出来。不过这里提一下SSH的配置,它把~/.ssh/id_rsa.pub这个公钥塞进到Linux的vagrant用于的home目录中,换句话说就是,本机需要配置了RSA秘钥对:

#
ssh-keygen -t rsa -C "you@homestead"

所以,box启动后可以直接使用本地私钥ssh到其中(ip:192.168.10.10, port:22, user:vagrant)。另外,box启动后也会动态的产生一个RSA秘钥对,并且这个公钥也会塞进vagrant用户,所以也可以使用这个动态产生的RSA秘钥对登录,只是机器销毁后这个秘钥对也会被删除,重启后又产生新的,多少有点不方便。

另外关于端口映射,比如8000转发到 80,实际就是把本机(127.0.0.1)的8000端口转发到客户机(192.168.10.10)的80端口上,看起来是虚拟机层次提供的实现。所以域名可以直接绑定到127.0.0.1,只是在端口号上要处理一下。

在虚拟机中共享宿主机文件

在VMware中,可以使用虚拟机本身提供的文件共享功能实现文件共享:
vmware-share
这里是选定宿主机需要曝光的文件夹。客户机要能读取到这个文件夹,还需要借助VMware提供的VMware Tool,进入客户机后,点击VMware的虚拟机 – 安装VMware Tool:
vmtool-install
1 如果是图形界面,一般就可以自动完成安装
2 如果非图形界面,可能需要手动安装

mount /dev/cdrom2 /mnt
cd /mnt
cp VMwareTools-xxxx.tar.gz ~
cd ~
tar zxvf VMwareTools-xxxx.tar.gz
cd vmware-tools-distrib
# 最终运行,根据提示完成安装
./vmware-install.pl

工具安装完毕。运行:

/usr/bin/vmhgfs-fuse .host:/  /mnt

这样共享文件夹就mount到了/mnt,进入/mnt应该就可以看到宿主机的共享文件了。不过如果重启,这个mount点就消失了,所以还需要如下操作:

#在CentOS 7中执行
chmod +x /etc/rc.d/rc.local

#添加
vi /etc/rc.local

/usr/bin/vmhgfs-fuse .host:/  /mnt

不过这视乎仅能做文件共享,当配置Nginx指向到共享文件夹时,提示权限问题而无法使用。

在VirtualBox中,其提供的文件共享功能就比较实用:
vbox
为了让共享文件可以工作,需要在客户机中安装增强工具。

除了利用虚拟软件提供的支持,如果宿主机是Windows,SMB文件共享方案是最好的选择,SMB在Windows下是内置的,客户机可以是Windows或Linux, 如果是Linux,一般需要安装cifs软件包:

yum search cifs
cifs-utils-6.2-9.el7.x86_64

#CentOS 
yum install cifs-utils
#Ubuntu
sudo apt-get install cifs-utils

#mount共享文件夹
mount -t cifs //192.168.1.121/www /var/www -o username=administrator,password='xxx',uid=1000,gid=1000

如果宿主机是Linux,客户机也是Linux,最好的共享方式就是使用NFS(NFS不支持Windows)。

SSH反向代理内网穿透

SSH可以用来做正向Socks代理外,还可以用来做反向代理。典型的应用场景是:让内网的机器对外网可见,常见手段是在路由器上做端口映射,但是由于路由器的IP地址会变化(除非你有固定不变的IP),所以还需要在内网或路由器上运行一个动态DNS的工具(这种工具实际就是往外网发送IP地址),然后通过域名获取到最新的IP,而SSH做反向代理就不需要这些步骤,当然,需要外网有一台机器作为接入点。

简单来说,就是内网机器和外网的机器建立一条TCP隧道,外网预期分配的端口通过SSHD服务转发到内网来,内网的数据出去也一样。TCP隧道,头尾都对应端口号,比如把外网的2222端口,通过TCP隧道跟内网的22端口接起来。

ssh -CNR 2222:127.0.0.1:22 -b 0.0.0.0 www@外网IP

然后输入外网IP的www用户的密码就可以建立一条TCP隧道。可以看到,这个时候外网IP的2222端口开放了,它是由SSHD服务开启的。这里的-b参数说明希望外网IP的监听地址是0.0.0.0,就是0.0.0.0:2222,不过可能看到的是127.0.0.1:2222,这个不是我们所期望的。为了让反向代理真正可靠的使用,还需要解决如下几个问题:

0 自动登录问题
这个问题最常见的是放置公钥,内网ssh通过-i来指定私钥文件。如果希望使用密码来登录,那么需要借助sshpass这个包装器,之所以需要它,那是因为ssh不支持通过参数指定密码:

yum install sshpass

#格式
sshpass -p "密码" ssh -CNR 3080:127.0.0.1:80 www@xx.xx.xx.xx

1 在外网监听任意地址
首先,内网ssh命令需要使用-b指定监听地址为0.0.0.0,然后外网的sshd服务的配置,需要修改GatewayPorts为yes:

vi /etc/ssh/sshd_config
GatewayPorts yes

可能还需要重启一下sshd服务。

2 TCP隧道长时保持连接
在内网上运行ssh命令时,加上-o TCPKeepAlive=yes来让TCP保持长时连接。

3 防止TCP被中断
发送大量的数据或长时没有数据发送时,都可能导致TCP连接被卡住或被中间的路由器杀掉。所以需要建立一个心跳检查,在内网运行ssh时添加-o ServerAliveInterval=10 -o ServerAliveCountMax=6来维持心跳,这里的意思是每10秒检测一次,如果连续6次都无法获取响应,那么进程将退出。

4 防止进程异常退出
通过设置TCP长链接和定时发送心跳数据来保存通道畅通,但是从实际使用来看,这些还是远远不够的。进程异常退出监控起来很简单,但是实际遇到过这种情况:通道实际已经无法通信,但是本地进程并未退出,这个情况可能是远程进程异常退出,但是本地进程并没有收到通知,或者中间路由的问题,所以为了解决这个问题,我们可以定时发送数据,根据响应来判断是否要重启本地进程,于是有了以下脚本:

#!/bin/bash

cmm="/usr/bin/kill"
if [ ! -f "$cmm" ]; then
    cmm="/bin/kill"
fi

#不等于空说明通道通畅
ping=`curl -s http://x.x.x.x:2222`
if [ "$ping" = "" ]; then
	ps aux | grep 'x.x.x.x' | grep -v grep | awk '{print $2"|"$6}' | cat | while read line
	do
    		pid=`echo $line | cut -d "|" -f 1`
    		rss=`echo $line | cut -d "|" -f 2`

        	kill=`$cmm -9 $pid`
	        date=`date "+%Y-%m-%d %H:%M:%S"`

	        echo $date' -- 'PID:$pid' - Was Killed.'
	done
fi

#反代
live=`ps -efH | grep 'ssh -CN -R 2222:127.0.0.1:22' | grep -v 'grep' | wc -l`
if [ $live -eq 0 ]; then
    ssh -CN -R 2222:127.0.0.1:22 -b 0.0.0.0 -o TCPKeepAlive=yes -o ServerAliveInterval=10 -o ServerAliveCountMax=6 sshproxy@x.x.x.x -p 22 > /dev/null 2>&1 &
fi

live=`ps -efH | grep 'ssh -CN -R 8000:127.0.0.1:8000' | grep -v 'grep' | wc -l`
if [ $live -eq 0 ]; then
    ssh -CN -R 8000:127.0.0.1:8000 -b 0.0.0.0 -o TCPKeepAlive=yes -o ServerAliveInterval=10 -o ServerAliveCountMax=6 sshproxy@x.x.x.x -p 22 > /dev/null 2>&1 &
fi

使用curl来发送数据,如果通道畅通就会有响应,否则就无响应,无响应则强制杀掉本地进程。紧接着重启进程。

这个方法虽然很粗暴,但是很实用。从运行来看,还是相当稳定的(实际我仅仅是把内网的一个服务映射出去,偶尔用一用,但是用的时候要保证能用,比如内网的Gitlab)。

Sockets代理 之 Shadowsocks

ShadowSocks – http://shadowsocks.org

实现一个Socks代理,很多语言都提供有内置支持或扩展包可用,需要编写的代码也不会太多。ShadowSocks这个工具提供了多种语言的实现,从http://shadowsocks.org/en/download/servers.html可以看到,有Python、Go、C with libev、C++ with Qt, 这些实现版本中,都应该包含服务端和客户端工具,不过也有一些专门的客户端(主要提供图形操作界面),比如
Windows客户端(https://github.com/shadowsocks/shadowsocks-windows/releases)
Linux下的客户端(https://github.com/shadowsocks/shadowsocks-qt5/releases)
Mac OS X下的客户端(https://github.com/shadowsocks/ShadowsocksX-NG/releases),Android下的客户端(https://github.com/shadowsocks/shadowsocks-android,在Android下可打开https://play.google.com/store/apps/details?id=com.github.shadowsocks进行安装)。这个代理软件与一般的Socks5代理稍不一样的是:它不是一个纯粹的代理,它加入了加密的功能。 这个工具开启的代理功能和SSH开启的代理有本质不一样,SSH开启的代理会在本地和服务端之间建立一条持久的TCP链接,而shadowsocks只是在服务器开启了监听端口,客户端的链接随到随链。所以,对于需要HOLD大量链接的情况,C with libev是一个很好的选择。

注:
由于shadowsock不是一个纯的Sockets代理,所以必须搭配客户端使用。

服务端,这里关注C实现的版本:https://github.com/shadowsocks/shadowsocks-libev,它提供了比较完整的功能。客户端关注Windows下的版本,它是.NET软件,可能需要升级你的.NET版本。

在CentOS 7.x上,可以使用了一个yum源来安装(参考https://copr.fedorainfracloud.org/coprs/librehat/shadowsocks/):

#https://copr.fedorainfracloud.org/coprs/librehat/shadowsocks/repo/epel-7/librehat-shadowsocks-epel-7.repo
#vi /etc/yum.repos.d/librehat-shadowsocks.repo

[librehat-shadowsocks]
name=Copr repo for shadowsocks owned by librehat
baseurl=https://copr-be.cloud.fedoraproject.org/results/librehat/shadowsocks/epel-7-$basearch/
type=rpm-md
skip_if_unavailable=True
gpgcheck=1
gpgkey=https://copr-be.cloud.fedoraproject.org/results/librehat/shadowsocks/pubkey.gpg
repo_gpgcheck=0
enabled=1
enabled_metadata=1

安装:

yum install shadowsocks

rpm -qa | grep shadowsocks
shadowsocks-libev-2.5.5-1.el7.centos.x86_64

rpm -ql shadowsocks-libev-2.5.5-1.el7.centos.x86_64
/etc/default/shadowsocks-libev
/etc/shadowsocks-libev/config.json
/usr/bin/ss-local
/usr/bin/ss-manager
/usr/bin/ss-nat
/usr/bin/ss-redir
/usr/bin/ss-server
/usr/bin/ss-tunnel
/usr/lib/systemd/system/shadowsocks-libev.service
/usr/lib64/libshadowsocks-libev.so

编译安装(新版本可能有问题)

# Ubuntu
apt-get install --no-install-recommends build-essential automake autoconf libtool libssl-dev libpcre3-dev asciidoc xmlto
# CentOS
yum install gcc autoconf libtool automake make zlib-devel openssl-devel asciidoc xmlto

#克隆源码,或直接下载压缩包
git clone https://github.com/shadowsocks/shadowsocks-libev.git
cd shadowsocks-libev
./configure --prefix=/usr/local/ss
make
make install

#######################################
# CentOS下编译安装新版本参考:https://shadowsocks.be/4.html,以下是从脚本中提取的安装步骤
#安装必须的软件包
yum install epel-release
yum install -y unzip openssl openssl-devel gettext gcc autoconf libtool automake make asciidoc xmlto udns-devel libev-devel pcre pcre-devel git c-ares-devel

#下载安装
wget https://github.com/jedisct1/libsodium/releases/download/1.0.13/libsodium-1.0.13.tar.gz
tar zxf libsodium-1.0.13.tar.gz
cd libsodium-1.0.13
./configure —prefix=/usr && make && make install

wget http://dl.teddysun.com/files/mbedtls-2.6.0-gpl.tgz
tar xf mbedtls-2.6.0-gpl.tgz
cd mbedtls-2.6.0-gpl
make SHARED=1 CFLAGS=-fPIC
make DESTDIR=/usr install

wget https://c-ares.haxx.se/download/c-ares-1.12.0.tar.gz
tar zxvf c-ares-1.12.0.tar.gz
cd c-ares-1.12.0
./configure
make && make install

ldconfig

wget https://github.com/shadowsocks/shadowsocks-libev/releases/download/v3.1.0/shadowsocks-libev-3.1.0.tar.gz
tar zxf shadowsocks-libev-3.1.0.tar.gz
cd shadowsocks-libev-3.1.0
./configure --disable-documentation
make && make install

默认配置在/etc/default/shadowsocks-libev,配置文件/etc/shadowsocks-libev/config.json,服务启动脚本/usr/lib/systemd/system/shadowsocks-libev.service。默认实际使用到的就是/usr/bin/ss-server(如果要启动客户端,修改为ss-local即可),查看配置文件:

{
    "server":"120.xx.xx.28",
    "server_port":8388,
    "local_port":1080,
    "password":"xxxxxxxx",
    "timeout":60,
    "method":"aes-256-cfb"
}

不管是使用ss-local和ss-server,这几个参数都一样,注意:作为服务端,local_port应该是没有用的。客户端和服务端,都预置了密码和对称加密使用的算法,简单来说,它们通过密码认证,然后使用预置的算法产生一个秘钥,用这个秘钥来加解密传输的数据。不难看出,它跟传统的CA不同,秘钥的交换不是通过加密隧道来交换的。如果需要安全,不要使用这个工具。

这个包也包括了其它工具:

/usr/bin/ss-local	客户端,和服务端的配置一样
/usr/bin/ss-manager	用来管理服务端,比如在服务端开启多个端口(动态拉起ss-server)
/usr/bin/ss-nat		管理NAT映射
/usr/bin/ss-redir	客户端工具,用来实现透明代理
/usr/bin/ss-server	服务端
/usr/bin/ss-tunnel	用来实现本地端口转发,比如目的地址符合条件时,转发到本地某个端口

对于客户端,最典型的应用,就是在内网架设一个服务器,上面安装ShadowSocks-livev,然后启动客户端(也可以把参数写入配置,用c指定读取的配置文件):

ss-local -s 120.xx.xx.x -p 8388 -b 192.168.1.250 -l 1080 -k xxxxx -m aes-256-cfb

需要代理的机器,指定为192.168.1.250即可,这样就可以避免在每个终端上安装客户端。不过,如果需要连接多个代理服务器,就需要启动多个ss-local进程,并且指定不同的端口。

注:可以在服务器上设置多用户(实际是启动多个ss-server,用端口区分),客户端可以配置多个远程服务器,但是需要指定默认,需要更换时,需要切换一下默认远程服务器,从这里可知,一个客户端要对应哪个远程是随意的,目前看起来缺少的是在客户端管理远程服务器的工具(比如只启动一个客户端,动态或安装一定规则对应到不同远程服务器)

Window下的客户端(https://github.com/shadowsocks/shadowsocks-windows/releases):
ss

当然,这个客户端还提供了 系统代理模式(就是直接修改系统代理) 系统代理模式(全局与PAC) 切换服务器等功能。

Linux下的客户端不支持配置多个端口(不同端口对应不同的服务端),所以可以启动多个客户端来解决,比如分别对应建立配置文件,然后写一段简单粗暴的定时脚本,就可以Hold它:

#!/bin/bash

live=`ps -efH | grep '/etc/shadowsocks-libev/config_4441.json' | grep -v 'grep' | wc -l`
if [ $live -eq 0 ]; then
    /usr/bin/ss-local -a root -c /etc/shadowsocks-libev/config_4441.json -u 2>&1 > /dev/null &
fi

live=`ps -efH | grep '/etc/shadowsocks-libev/config_4442.json' | grep -v 'grep' | wc -l`
if [ $live -eq 0 ]; then
    /usr/bin/ss-local -a root -c /etc/shadowsocks-libev/config_4442.json -u 2>&1 >> /dev/null &
fi
 
live=`ps -efH | grep '/etc/shadowsocks-libev/config_4443.json' | grep -v 'grep' | wc -l`
if [ $live -eq 0 ]; then
    /usr/bin/ss-local -a root -c /etc/shadowsocks-libev/config_4443.json -u 2>&1 >> /dev/null &
fi

live=`ps -efH | grep '/etc/shadowsocks-libev/config_4444.json' | grep -v 'grep' | wc -l`
if [ $live -eq 0 ]; then
    /usr/bin/ss-local -a root -c /etc/shadowsocks-libev/config_4444.json -u 2>&1 >> /dev/null &
fi

exit 0

需要说明的是shadowsocks-qt5是一个跨平台的客户端(https://github.com/shadowsocks/shadowsocks-qt5/releases),以下链接是安装和使用说明:https://github.com/shadowsocks/shadowsocks-qt5/wiki:

Ubuntu通过PPA方式安装:

sudo add-apt-repository ppa:hzwhuang/ss-qt5
sudo apt-get update
sudo apt-get install shadowsocks-qt5

这个ppa对应的链接是:https://launchpad.net/~hzwhuang/+archive/ubuntu/ss-qt5
ss-ubuntu

而对应的Window版本长这个样子:
ss-window

最后:
作为服务端,如果使用的是Linux(CentOS或Ubuntu),最好的选择是shadowsocks-libev,而对于Windows,看起来只有一个选择(https://github.com/shadowsocks/libQtShadowsocks/releases,提供一个叫libQtShadowsocks的工具,既可做服务端,也可以做客户端); 作为客户端,在Windows和Ubuntu下https://github.com/shadowsocks/shadowsocks-qt5/releases这个较好。

这是一个百花齐放的开源产品。

Stunnel构建加密隧道

最新版本可到:http://www.stunnel.org/downloads.html下载,Stunnel工作过程就是在客户端和服务端之间建立一条加密通道,所有Stunnel有两个工作模式,默认工作模式是服务端,如果需要作为客户端,那么就是需要使用client=yes来指定。客户端加密数据需要一个证书(可以自签发),服务端需要私钥,以解密数据,这个工作过程就是HTTPS的工作流程。

一般使用yum或者apt-get安装即可。

##############################################
###
apt-get update
apt-get install openssl

###
apt-get install stunnel

#开机启动,ENABLED改为1
vi /etc/default/stunnel4
ENABLED=1

#
cd /etc/stunnel
vi stunnel.conf

[squid]
accept = 4041 
connect = 127.0.0.1:4040
cert = /etc/stunnel/stunnel.pem

#产生证书 私钥 - 实际就是产生一对RSA秘钥,公钥在证书中
openssl genrsa -out key.pem 2048
openssl req -new -x509 -key key.pem -out cert.pem -days 3650
cat key.pem cert.pem >> /etc/stunnel/stunnel.pem

#启动
/etc/init.d/stunnel4 restart


##############################################
## 客户端
client = yes
[squid]
accept = 127.0.0.1:4444
connect = [服务端IP]:4041
cert = D:/stunnel.pem

添加监控脚本:

cat stunnel.sh 
#!/bin/bash

live=`ps -efH | grep '/usr/bin/stunnel' | grep -v 'grep' | wc -l`
if [ $live -eq 0 ]; then
    /usr/bin/stunnel 2>&1 >> /dev/null
fi

Windows下下载一个GUI工具:
stunnel

默认Stunnel以服务器为工作模式,所以在作为客户端时务必添加client = yes。

对于客户端,可以开启多个端口,分别指向多个代理:

client = yes
cert= E:/var/stunnel.pem

[fq1]
accept = 4441
connect = 47.90.x.43:4041

[fq2]
accept = 4442
connect = 47.90.x.152:4041

[fq3]
accept = 4443
connect = 47.90.x.140:4041

[fq4]
accept = 4444
connect = 47.90.x.215:4041

HTTP代理 之 Squid与Node.js

实现HTTP代理有两种方式:
一种是普通代理,代理服务器充当的是一个中间的组件。这种方式简单来说就是接受来自客户端的HTTP报文(原本不应该发送给代理服务器的,强制发送给代理服务器),然后在服务器端做一些必要修改再转发出去(可能涉及via头,请求uri,来源ip等)。这种方式一般认为不能代理HTTPS流量。Nginx、Squid支持这种代理转发。

另一种是TCP隧道代理。这种代理可以以HTTP协议的方式来实现理论上任意TCP之上的应用层协议代理(可以代理HTTPS)。Squid支持这种方式,而Nginx不支持这种方式。

在Node.js中,实现这两种代理方式,只需数行代码。


普通代理(正向代理)

###Nginx HTTP正向代理示例
server {
    resolver 8.8.8.8;
    resolver_timeout 5s;
 
    listen x.x.x.x:8080;
 
    location / {
        proxy_pass $scheme://$host$request_uri;
        proxy_set_header Host $http_host;
    }
}

###Squid HTTP正向代理示例
acl localnet src 0.0.0.0/0

acl SSL_ports port 443
acl Safe_ports port 80

acl CONNECT method CONNECT

http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports

http_access allow localnet
http_access allow localhost
http_access deny all

http_port 8080

#举例
# 1 原始HTTP报文
GET /action?b=123 HTTP/1.1
Host: blog.ifeeline.com
注:通过浏览器的代理后,GET /action?b=123 HTTP/1.1会被变成GET http://blog.ifeeline.com/action?b=123 HTTP/1.1

# 2 这个报文原样转发给x.x.x.x:8080,原本应该是直接发给blog.ifeeline.com的(Host字段)

# 3 根据HOST DNS到IP,把这个Http报文转发出去(如必要可做必要修改,比如添加请求头)

在Node.js中,这种方式的代理转发,实现非常简单:

var http = require('http');
var url = require('url');

function request(req, res) {
    var u = url.parse(req.url);
	
    //var options = {
	//  hostname : u.hostname, 
	//  port     : u.port || 80,
	//  path     : u.path,       
	//  method   : req.method,
	//  headers  : req.headers
    //};

    // 继续发给其它代理(比如Squid Nginx)  
    var options = {
        hostname : "120.xx.xx.192", 
        port     : 3333,
        path     : 'http://'+u.host+u.path,       
        method   : req.method,
        headers  : req.headers
    };

    var ireq = http.request(options, function(ires) {
        res.writeHead(ires.statusCode, ires.headers);
        ires.pipe(res);
    }).on('error', function(e) {
        res.end();
    });

    req.pipe(ireq);
}

http.createServer().on('request', request).listen(8080, '0.0.0.0');

另一种代理方式是隧道代理,目前应用非常的广泛,实际上原来非常的简单:HTTP客户端(浏览器)通过CONNECT方法请求代理服务器创建一条到达目的服务器和端口的TCP连接,然后把HTTP客户端(浏览器)传输过来的数据通过这个TCP连接直接转发。

很明显,相比之下,多了一次来回(CONNECT请求)。而后续的操作就是把数据从一个链接转到另一个链接,仅仅是转发TCP数据,实际就是把前面的输出作为后面的输入,如此而已。

Nginx不支持CONNECT请求,自然就无法支持隧道代理了。Squid支持隧道代理。在Node.js中用数行代码就可以实现隧道代理:

var http = require('http');
var net = require('net');
var url = require('url');

function connect(req, sock) {
    var u = url.parse('http://' + req.url);

    var insock = net.connect(u.port, u.hostname, function() {
        sock.write('HTTP/1.1 200 Connection Established\r\n\r\n');
	// 回传客户端
        insock.pipe(sock);
    }).on('error', function(e) {
        sock.end();
    });
    // 客户端传送数据
    sock.pipe(insock);
}

http.createServer().on('connect', connect).listen(8080, '0.0.0.0');

代理接收到connect方法请求后,建立一条到目标服务器的TCP链接,然后向客户端回送确认,客户端得到确认后继续使用sock把数据发送过来,代理服务器把从sock过来的数据推送到目标服务器。

一个基本事实:
浏览器设置HTTP代理时,如果是HTTP链接,则把HTTP报文直接发送到代理服务器。如果是HTTPS链接,则采用以上描述的隧道方式,先发送一个connet请求,然后发送数据。大体意思是能用普通代理的就不用隧道代理,毕竟隧道代理多了一个来回。(并不是HTTP链接不能走隧道代理)

把以上两段Node.js代码合并一起就可以实现全功能的HTTP代理:

var http = require('http');
var net = require('net');
var url = require('url');

function request(req, res) {
    var u = url.parse(req.url);
	var options = {
	    hostname : u.hostname, 
	    port     : u.port || 80,
	    path     : u.path,       
	    method   : req.method,
	    headers  : req.headers
	};
    };

    var ireq = http.request(options, function(ires) {
        res.writeHead(ires.statusCode, ires.headers);
        ires.pipe(res);
    }).on('error', function(e) {
        res.end();
    });

    req.pipe(ireq);
}

function connect(req, sock) {
    var u = url.parse('http://' + req.url);

    var insock = net.connect(u.port, u.hostname, function() {
        sock.write('HTTP/1.1 200 Connection Established\r\n\r\n');
	// 回传客户端
        insock.pipe(sock);
    }).on('error', function(e) {
        sock.end();
    });
    // 客户端传送数据
    sock.pipe(insock);
}

http.createServer().on('request', request).on('connect', connect).listen(8080, '0.0.0.0');

对于HTTPS链接的代理,从客户端到代理服务器,如果走的是HTTP,实际也不会增加安全问题,除了connect链接暴露的目标服务器和端口号外,后续的数据是加密的,中间人不可能解密。

如果确实担心这个问题,可以签发自签名的证书。 让客户端到代理之间也走HTTPS:

openssl genrsa -out root.pem 2048
openssl req -new -x509 -key root.pem -out root.crt -days 99999

注意:Common Name务必填写代理服务器IP。

把root.crt下载下来添加到系统受信任根证书列表中,完成这个步骤后从浏览器和代理,代理和目标服务器之间就可以建立安全链接了(虽然过程有伪造)。

对于以上Node.js的代码,只需要修改一点点:

var options = {
    key: fs.readFileSync('./root.pem'),
    cert: fs.readFileSync('./root.crt')
};

https.createServer(options).on();

添加根证书,http改为https而已。

—————————————————————-
以下是一个Squid代理配置实例:

via off
forwarded_for transparent
dns_nameservers 8.8.4.4 8.8.8.8

## IP白名单
acl localnet src 120.x.x.x
## IP白名单_公司内网
acl localnet src x.x.0.0/16
acl localnet src x.x.0.0/16
## IP白名单_本地网络
acl localnet src 10.0.0.0/8
acl localnet src 172.16.0.0/12
acl localnet src 192.168.0.0/16

## SSL端口
acl SSL_ports port 443

## 安全端口
acl Safe_ports port 80		# http
acl Safe_ports port 21		# ftp
acl Safe_ports port 443		# https
acl Safe_ports port 70		# gopher
acl Safe_ports port 210		# wais
acl Safe_ports port 1025-65535	# unregistered ports
acl Safe_ports port 280		# http-mgmt
acl Safe_ports port 488		# gss-http
acl Safe_ports port 591		# filemaker
acl Safe_ports port 777		# multiling http

## 启用HTTP隧道代理 
acl CONNECT method CONNECT

## 非安全端口
http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports

## 运行IP白名单
http_access allow localnet
http_access allow localhost
http_access deny all

## 端口监听
http_port 4040

coredump_dir /var/spool/squid3

refresh_pattern ^ftp:		1440	20%	10080
refresh_pattern ^gopher:	1440	0%	1440
refresh_pattern -i (/cgi-bin/|\?) 0	0%	0
refresh_pattern .		0	20%	4320

参数via off和forwarded_for transparent是建立透明代理的基础。目标机器就无法完全感知是被代理了。

由于内容直接使用HTTP转发,不管原始链接是HTTP还是HTTPS,目标IP与域都是暴露的,所以一旦被过滤,就会出现连接被重置的提示,所以,为了避免过滤,需要做加密。默认安装的Squid并不支持开启https端口,所以需要重新编译。可以使用Stunnel来建立加密隧道,然后由Stunnel解密后转发给Squid,这个做法很常见。