月度归档:2016年10月

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,这个做法很常见。

Sockets代理 之 SSH Sockets

使用SSH来开启Socks代理:

ssh -CN -D 127.0.0.1:7070 root@x.x.x.x -p 22
root@x.x.x.x's password:

这样就可以启动一个Socks,C表示压缩数据传输,N表示不进入Bash。(比如浏览器)设置Socket代理IP为127.0.0.1端口为7070,然后就可以通过x.x.x.x(x.x.x.x可以是本机地址)代理上网了。

SSH可以通过-i指定一个私钥来进行认证(这样就不需要输入密码),这种方式意味在需要在服务器端对应的用户里放置一个公钥:

root@xxx:~/.ssh ls
authorized_keys  known_hosts
root@xxx:~/.ssh# pwd
/root/.ssh
root@xxx:~/.ssh:~/.ssh# ls
authorized_keys  known_hosts
root@xxx:~/.ssh:~/.ssh# cat authorized_keys 
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCiaMoJRe..........

公钥放进去后,在客户端就需要指定其对应的私钥:

ssh -CN -D 127.0.0.1:7070 root@x.x.x.x -p 22 -i /roo/is_rsa

这样就不需要输入密码了。Linux中SSH不支持直接输入密码,但是可以使用一个包装工具来完成(sshpass):

yum install sshpass

#运行sshpass
sshpass -p "passwd" ssh -CN -D 127.0.0.1:7070 root@x.x.x.x -p 22 

#查看进程可见:sshpass就是一个包装器
sshpass -p zzzzzzzzzz ssh -CN -D 127.0.0.1:7070 root@127.0.0.1 -p 22
ssh -CN -D 127.0.0.1:7070 root@127.0.0.1 -p 22

注:这里是监听本机的127.0.0.1的7070端口,本机通过SSH把数据传输到远程服务器,把远程服务器设置在本机也是可以的,这样本机和远程都一样的IP。

为了防止进程异常退出,可以定时运行一个SHELL脚本:

#1 使用sshpass包装器

#!/bin/bash

live=`ps -efH | grep 'sshpass' | grep -v 'grep' | wc -l`
if [ $live -eq 0 ]; then
    sshpass -p "passwd" ssh -CN -D 127.0.0.1:7070 root@x.x.x.x -p 22 2>&1 >> /dev/null
fi


#2 不使用sshpass包装器时,需要在远程服务器上设置放置好公钥
#!/bin/bash

live=`ps -efH | grep 'ssh -CN -D' | grep -v 'grep' | wc -l`
if [ $live -eq 0 ]; then
    ssh -CN -D 127.0.0.1:7070 root@x.x.x.x -p 22 2>&1 >> /dev/null
fi

本地和远程之间传输数据实际是经过加密的(走SSH),就算本机和远程是同一条机器时也是如此。本地和客户端建立的TCP是一条持久链接:

netstat -ntp

Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name   
tcp        0      0 127.0.0.1:22            127.0.0.1:39426         ESTABLISHED 20575/sshd: root                    
tcp        0      0 127.0.0.1:39426         127.0.0.1:22            ESTABLISHED 20574/ssh  

客户端ssh随机打开了一个端口(39436)和远程22端口进行链接。所有从7070过来的数据,都会转发到39436端口,由它把数据发送到服务端。不过这个本地转发是透明的。到此,我们有两个结论:
一、所有的数据都是通过一条持久链接发送到远程的
二、由SSH开放的端口(这里是7070),需要HOLD住所有的链接,等到服务端返回后,再转发回去。
所以,SSH开启的代理,不适用于高并发场景。对于高并发的场景,代理需要HOLD住大量链接,然后在和服务端链接时应该一一对应,或者开一个池。但是对于一般的代理上网,是够用的。

在Windows下也有对应的工具,下载地址:http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html。这里有一个叫Plink的工具,它类似sshpass的功能:

plink.exe -C -N -D 127.0.0.1:7070 root@x.x.x.x -pw password -P 22 -v

参数C表示启用压缩,N表示不进入终端,-pw来指定密码,-P指定端口(注意这里是大写的P), -v表示实时的输出日志。使用Plink这个工具(或者Putty)也可以通过-i来指定私钥。

监控脚本,在Windows下:

:1
bin\plink.exe -C -N -D 127.0.0.1:7070 root@x.x.x.x -pw password -P 22 -v
goto 1

保存为一个bat文件,需要代理时,双击它,然后弹出一个命令行窗口,由于加了-v参数,可以实时参考输出日志(证明它是工作的),不需要时直接关闭即可。

最后就是浏览器的代理配置了(以火狐为例子):
工具 – 选项
firefox-setting

firefox-agent

看起来,虽然实现了使用SSH Socks来上网,但是还是稍有麻烦:
1 不使用代理时,需要重新进入 工具 – 选项,找到网络,然后点击不使用代理
2 每次要上网代理时,需要运行一个命令行窗口

为了解决第一个麻烦问题,我们可以在firefox中安装一个叫autoproxy的插件(也有其它类似的),安装之后浏览器上会有一个按钮,点击切换是否使用代理:
autoproxy
可以配置多个代理,方便切换:
autoproxy-switch
Autoproxy这个插件看起来从2013年开始就再没有更新了。

至于第二个问题,我们需要找到一个客户端管理工具。目前在使用的工具:
1 http://nemesis2.qx.net/pages/MyEnTunnel (需要翻墙)
下载的是一个exe安装文件,一路安装下来:
myentunnel
其中lng是语言文件,ini是配置文件,可以发现plink.exe,所以这个工具是plink.exe包装器,让使用plink.exe使用更加方便一点而已。双击myentunnel.exe:
myentunnel-setting
这里的配置跟plink.exe的命令使用是对应的。可以设置多个配置,默认的是叫default的配置,可以点击右下角的红锁(或绿锁),添加配置,这个配置实际是对应命令目录下的ini文件。看来,配置无法删除,只能到文件夹里面删除对应的文件。

可以把这个文件夹拷贝下来。

2 另一个可用的工具是Bitvise Tunnelier,它不是plink.exe的包装器,可以互联网搜索看看。

PHP7.0.x编译

PHP安装根据启用的PHP模块,对系统有不同的库文件依赖,一般情况如下:

gd gd-devel			--with-gd --enable-gd-native-ttf
libjpeg libjpeg-devel		--with-jpeg-dir
libpng libpng-devel		--with-png-dir
freetype freetype-devel		--with-freetype-dir

libxml2 libxml2-devel		--enable-xml

libmcrypt libmcrypt-devel	--with-mcrypt
libmhash libmhash-devel		--with-mhash

curl curl-devel			--with-curl

zlib zlib-devel			--with-zlib
openssl openssl-devel		--with-openssl
pcre pcre-devel			PHP的正则功能依赖

安装:

#基本	
yum install libtool autoconf automake gcc gcc-c++ make

#依赖库
yum install gd gd-devel libjpeg libjpeg-devel libpng libpng-devel freetype freetype-devel libxml2 libxml2-devel libmcrypt libmcrypt-devel libmhash libmhash-devel curl curl-devel zlib zlib-devel openssl openssl-devel pcre pcre-devel

#编译参考
./configure \
--prefix=/usr/local/php7 \
--exec-prefix=/usr/local/php7 \
--bindir=/usr/local/php7/bin \
--sbindir=/usr/local/php7/sbin \
--includedir=/usr/local/php7/include \
--libdir=/usr/local/php7/lib/php \
--mandir=/usr/local/php7/php/man \
--with-config-file-path=/usr/local/php7/etc \
--with-mysql-sock=/var/lib/mysql/mysql.sock \
--with-mcrypt=/usr/include \
--with-mhash \
--with-openssl \
--with-mysqli=shared,mysqlnd \
--with-pdo-mysql=shared,mysqlnd \
--with-gd \
--with-iconv \
--with-zlib \
--enable-zip \
--enable-inline-optimization \
--disable-debug \
--disable-rpath \
--enable-shared \
--enable-xml \
--enable-bcmath \
--enable-shmop \
--enable-sysvsem \
--enable-mbregex \
--enable-mbstring \
--enable-ftp \
--enable-gd-native-ttf \
--enable-pcntl \
--enable-sockets \
--with-xmlrpc \
--enable-soap \
--without-pear \
--with-gettext \
--enable-session \
--with-curl \
--with-jpeg-dir \
--with-freetype-dir \
--enable-opcache \
--enable-fpm \
--disable-cgi \
--with-fpm-user=www \
--with-fpm-group=www \
--without-gdbm \
--disable-fileinfo

安装:

Installing shared extensions:     /usr/local/php7/lib/php/extensions/no-debug-non-zts-20151012/
Installing PHP CLI binary:        /usr/local/php7/bin/
Installing PHP CLI man page:      /usr/local/php7/php/man/man1/
Installing PHP FPM binary:        /usr/local/php7/sbin/
Installing PHP FPM config:        /usr/local/php7/etc/
Installing PHP FPM man page:      /usr/local/php7/php/man/man8/
Installing PHP FPM status page:   /usr/local/php7/php/php/fpm/
Installing phpdbg binary:         /usr/local/php7/bin/
Installing phpdbg man page:       /usr/local/php7/php/man/man1/
Installing build environment:     /usr/local/php7/lib/php/build/
Installing header files:           /usr/local/php7/include/php/
Installing helper programs:       /usr/local/php7/bin/
  program: phpize
  program: php-config
Installing man pages:             /usr/local/php7/php/man/man1/
  page: phpize.1
  page: php-config.1
/usr/local/php7/src/build/shtool install -c ext/phar/phar.phar /usr/local/php7/bin
ln -s -f phar.phar /usr/local/php7/bin/phar
Installing PDO headers:           /usr/local/php7/include/php/ext/pdo/

Nginx配置应用实例汇总

防盗链
Nginx中防盗链是依靠valid_referers指令来完成的,一般是放在server或location段中:

location ~ .*\.(wma|wmv|asf|mp3|mmf|zip|rar|jpg|gif|png|swf|flv)$ {
     valid_referers none blocked *.ifeeline.com;
     if ($invalid_referer) {
      		return 403;
     }
}

其中的none表示空的来路,也就是直接访问,blocked表示被防火墙标记过的来路。

在正常访问中,HTTP的请求头都会返回一个referfer的参数,它标记当前的请求的来源,如果为空表示直接访问。

如果当前请求是http://blog.ifeeline.com/refer.php,里面嵌入了很多图片,图片会发起新的请求,那么这些图片的请求中就有一个referer的参数,它的值就是前面这个URL。另外,如果从一个连接点击跳转到另一个连接,那么后一个连接的referer的值就是前一个的连接。所以,当别人的网页盗用你的图片地址时,referer记录的就是盗用你图片的域。

注意:referer的值是否发送,完全是有浏览器决定的,目前大部分浏览器都主动发送这个referer,但是也有浏览器发送的referer不准确。

对于一般访问,这种防盗链的方法还是有效的,但是想对付程序采集就有点难度了。因为referer可以被伪造:

function GrabImage($url, $filename = "",$referer = ""){
	if($url == ""){return false;}
	$extt = strrchr($url, ".");
	$ext = strtolower($extt);
	if($ext != ".gif" && $ext != ".jpg" && $ext != ".png" && $ext != ".bmp"){echo $url."-格式不支持!";return false;}
	
	if($filename == ""){ $filename = time()."$extt"; }//以时间戳另起名
	
	ob_start();
	
	//-----------
	$ch = curl_init();
	curl_setopt($ch, CURLOPT_URL, $url);
	if($referer != ""){
		curl_setopt($ch, CURLOPT_REFERER, $referer);
	}
	curl_exec($ch);
	curl_close($ch);
	//----------
	
	$img = ob_get_contents();
	ob_end_clean();
	
	$size = strlen($img);
	$fp = fopen($filename , "a");
	fwrite($fp, $img);
	fclose($fp);
	
	return $filename;
}

可见,可以通过PHP的curl模块发送连接(伪造一个合法的referer),这样反盗链的设置就被跳过了。

Magento2.x Nginx + SSL 配置

Bash Shell脚本实例

0 ——-

#!/bin/bash

#定位KILL命令
cmm="/usr/bin/kill"
if [ ! -f "$cmm" ]; then
    cmm="/bin/kill"
fi

#保持多少个进程
hold=2
if [ -n "$1" ]; then
    hold=$1
fi

#内存占用阈值(100M)
threshold=102400

#获取具体路径
pwd=$(cd `dirname $0`; pwd)

#搜索进程
search=$pwd'/artisan queue:work --queue=adc'

#循环杀掉超过阈值的进程
ps aux | grep $search | 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`
    if [ $rss -gt $threshold ]; then
        kill=`$cmm -9 $pid`
        date=`date "+%Y-%m-%d %H:%M:%S"`
        echo $date' - 'PID:$pid' - RSS('$rss'K) > Threshold('$threshold'K) - Was Killed.'
    fi
done

#杀掉后维持进程数量
live=`ps -efH | grep $search | grep -v 'grep' | wc -l`
if [ $live -lt $hold ]; then
    for ((i=1; i<=$hold-$live; i=i+1))
    do
        php $search' --tries=3 --sleep=2 --timeout=300' 2>&1 >> /dev/null &
    done
fi

exit 0





#!/bin/bash

#定位KILL命令
cmm="/usr/bin/kill"
if [ ! -f "$cmm" ]; then
    cmm="/bin/kill"
fi

#操作系统位数
bit='32'
if [ $(getconf WORD_BIT) = '32' -a $(getconf LONG_BIT) = '64' ]; then
    bit='64'
fi

#端口
port=9515
if [ -n "$1" ]; then
    port=$1
fi

#获取具体路径
pwd=$(cd `dirname $0`; pwd)

#搜索进程
search=$pwd/chromedriver_linux$bit' --port='$port

#杀掉原来进程
ps aux | grep $search | grep -v grep | awk '{print $2}' | cat | while read pid
do
    kill=`$cmm -9 $pid`
    date=`date "+%Y-%m-%d %H:%M:%S"`
    echo $date' - 'PID:$pid' Was Killed.'
done

#重启进程
$pwd/chromedriver_linux$bit --port=$port 2>&1 >> /dev/null &

exit 0

##############################################################
##定时杀firefox

#!/bin/bash

#定位KILL命令
cmm="/usr/bin/kill"
if [ ! -f "$cmm" ]; then
    cmm="/bin/kill"
fi

#循环杀掉进程
ps aux | grep 'firefox/firefox' | 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' - RSS('$rss'K) Threshold('$threshold'K) - Firefox Was Killed.'
done

exit 0

1 内存监控,超过阀值则杀掉:

vi kill.sh

#!/bin/bash

#内存占用超过120M
threshold=120000
cmm="/usr/bin/kill"
if [ ! -f "$cmm" ]; then
    cmm="/bin/kill"
fi

ps aux | grep 'artisan queue:work' | 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`

    if [ $rss -gt $threshold ]; then
        kill=`$cmm -9 $pid`
	date=`date "+%Y-%m-%d %H:%M:%S"`
        if [ $kill ]; then
            echo $date' -- 'PID:$pid' - RSS('$rss'K) > threshold('$threshold'K) - Was Killed.'
        elif
            ech $date' -- 'PID:$pid' - RSS('$rss'K) > threshold('$threshold'K) - Kill Failed.' 
        fi
    fi
done

这里被杀掉的进程由于有使用进程监控器监控,被杀掉后会自动重启。

2 内存监控,超过阀值则杀掉并重启

#!/bin/bash

#判断进程数量
live=`ps -efH | grep 'agent.js' | grep -v 'grep' | wc -l`
if [ $live -eq 0 ]; then
	/usr/local/bin/node /var/www/ebay-ap/agent.js 2>&1 >> /var/www/ebay-ap/log.txt &
fi

#判断是否超过阀值
threshold=204800

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

ps aux | grep 'agent.js' | 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`

    if [ $rss -gt $threshold ]; then
        date=`date "+%Y-%m-%d %H:%M:%S"`
        kill=`$cmm -9 $pid`
        if [ $kill ]; then
            echo $date -- PID: ${pid} Kill Failed [RSS: ${rss} gt ${threshold}]
        else
            echo $date -- PID: ${pid} Kill Success [RSS: ${rss} gt ${threshold}]
            /usr/local/bin/node /var/www/ebay-ap/agent.js 2>&1 >> /var/www/ebay-ap/log.txt &
        fi
    fi
done

如果进程异常退出了,则自动拉起。然后判断内存是否超标,如果超标则先杀掉然后再重启。这个方式看起来粗暴了一些,不过是非常有效的。

3 封杀某个区域的IP

#下载中国IP库
wget http://www.ipdeny.com/ipblocks/data/countries/cn.zone

#运行以下脚本
#!/bin/bash

COUNTRY = "cn"
IPTABLES = /sbin/iptables
EGREP = /bin/egrep

if [ "$(id -u)" != "0" ]; then
   echo "you must be root" 1>&2
   exit 1
fi

resetrules() {
    $IPTABLES -F
    $IPTABLES -t nat -F
    $IPTABLES -t mangle -F
    $IPTABLES -X
}

resetrules

for c in $COUNTRY
do
        country_file = $c.zone

        IPS = $($EGREP -v "^#|^$" $country_file)
        for ip in $IPS
        do
           echo "blocking $ip"
           $IPTABLES -A INPUT -s $ip -j DROP
        done
done

exit 0

4 防DD脚本

#!/bin/sh

IGNORE_IP_LIST="/usr/local/ddos/ignore.ip.list"
IPT="/sbin/iptables"

KILL=1

# NO_OF_CONNECTIONS默认是150,这是一个经验值,如果服务器性能比较高,可以设置200以上,以避免误杀
NO_OF_CONNECTIONS=150

# 解锁的时间,单位为秒,可以设置更长时间
BAN_PERIOD=86400

# 解除对IP的封锁 过程就是运行一个睡眠脚本
unbanip()
{
	# 产生解除IP封锁时用到的随机脚本文件
        UNBAN_SCRIPT=`mktemp /tmp/unban.XXXXXXXX`

	# 临时文件
        TMP_FILE=`mktemp /tmp/unban.XXXXXXXX`

	# 将被解除封锁的IP
        UNBAN_IP_LIST=`mktemp /tmp/unban.XXXXXXXX`

	# 产生解除IP封锁的脚本内容
        echo '#!/bin/sh' > $UNBAN_SCRIPT
	# $BAN_PERIOD睡眠时间,表示$UNBAN_SCRIPT睡眠多久后继续
        echo "sleep $BAN_PERIOD" >> $UNBAN_SCRIPT

	# 输入重定向,行对于$line变量,是当前需要堵塞的IP,脚本运行过程中产生
	while read line; do
		# 解除IP封锁
		echo "$IPT -D INPUT -s $line -j DROP" >> $UNBAN_SCRIPT

		# 把将要解除堵塞的IP写入$UNBAN_IP_LIST
		echo $line >> $UNBAN_IP_LIST
	done < $BANNED_IP_LIST

        # 从$IGNORE_IP_LIST中去掉$UNBAN_IP_LIST,把结果写入$TMP_FILE(差集)
        echo "grep -v --file=$UNBAN_IP_LIST $IGNORE_IP_LIST > $TMP_FILE" >> $UNBAN_SCRIPT

	# 移动$TMP_FILE到$IGNORE_IP_LIST
        echo "mv $TMP_FILE $IGNORE_IP_LIST" >> $UNBAN_SCRIPT

	# 删除$UNBAN_SCRIPT
        echo "rm -f $UNBAN_SCRIPT" >> $UNBAN_SCRIPT
	# 删除$UNBAN_IP_LIST
        echo "rm -f $UNBAN_IP_LIST" >> $UNBAN_SCRIPT
	# 删除$TMP_FILE,经过上面的移动操作,$TMP_FILE其实已经不存在
        echo "rm -f $TMP_FILE" >> $UNBAN_SCRIPT

	# 在后台运行$UNBAN_SCRIPT
	. $UNBAN_SCRIPT &
}


TMP_PREFIX='/tmp/ddos'
TMP_FILE="mktemp $TMP_PREFIX.XXXXXXXX"

# 产生临时文件,存放已经被堵塞的IP
BANNED_IP_LIST=`$TMP_FILE`

# 产生临时文件,存放当前可能被堵塞的IP
BAD_IP_LIST=`$TMP_FILE`

#取回IP列表并按照数量从高到底排序
netstat -ntu | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -nr > $BAD_IP_LIST

# 如果配置为需要堵塞
if [ $KILL -eq 1 ]; then            
	# 超过100就记录
	NUM_CONNECTIONS=100  
	# 按照日期存放
	CNTS_LOG="/usr/local/ddos/$(date +%Y)/$(date +%m)/"
	mkdir -p $CNTS_LOG
	CNTS_LOG="$CNTS_LOG$(date +%Y%m%d).log"
	while read line; do
		CURR_CONN=$(echo $line | cut -d" " -f1)
		CURR_IP=$(echo $line | cut -d" " -f2)
		if [ $CURR_CONN -lt $NUM_CONNECTIONS ]; then
			break
		fi
		echo "$CURR_IP with $CURR_CONN connections at `date`" >> $CNTS_LOG
	done < $BAD_IP_LIST

        IP_BAN_NOW=0
        while read line; do
		# 当前这个IP有多少个连接
                CURR_LINE_CONN=$(echo $line | cut -d" " -f1)            
		# 当前IP
                CURR_LINE_IP=$(echo $line | cut -d" " -f2)          

		# 如果这个IP链接数小于预设,终止(因为数据经过排序)
                if [ $CURR_LINE_CONN -lt $NO_OF_CONNECTIONS ]; then     
			break
                fi

		# 计算当前IP在$IGNORE_IP_LIST出现了多少次
                IGNORE_BAN=`grep -c $CURR_LINE_IP $IGNORE_IP_LIST`
		# 如果当前IP已经在$IGNORE_IP_LIST,跳过,可以通过这个方式设置永不堵塞某些IP
                if [ $IGNORE_BAN -ge 1 ]; then
			continue
                fi

		# 进入到这里表示当前必定有IP要被堵塞
                IP_BAN_NOW=1

		# 添加堵塞了的IP到当前堵塞列表,$BANNED_IP_LIST会应用到unbanip函数
                # 如果不实现自动解锁,注释如下行
                echo $CURR_LINE_IP >> $BANNED_IP_LIST

		# 添加堵塞了的IP到$IGNORE_IP_LIST
                # 如果不实现自动解锁,注释如下行
                echo $CURR_LINE_IP >> $IGNORE_IP_LIST
                
		# 开始iptables封锁
                $IPT -I INPUT -s $CURR_LINE_IP -j DROP
                
        done < $BAD_IP_LIST

	# $IP_BAN_NOW等于1表示有IP被封锁了
        if [ $IP_BAN_NOW -eq 1 ]; then
		# 发送通知

		# 同时开始运行解除堵塞程序
                #unbanip                             
        fi
fi

# 清除临时产生的文件
rm -f $TMP_PREFIX.*

函数unbanip实际就是运行一个睡眠脚本,可以把其去掉(不自动解封):

#!/bin/sh

IGNORE_IP_LIST="/usr/local/ddos/ignore.ip.list"
IPT="/sbin/iptables"
KILL=1
NO_OF_CONNECTIONS=150

TMP_PREFIX='/tmp/ddos'
TMP_FILE="mktemp $TMP_PREFIX.XXXXXXXX"


BAD_IP_LIST=`$TMP_FILE`
netstat -ntu | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -nr > $BAD_IP_LIST

if [ $KILL -eq 1 ]; then
        IP_BAN_NOW=0
        while read line; do
		# 当前这个IP有多少个连接
                CURR_LINE_CONN=$(echo $line | cut -d" " -f1)            
		# 当前IP
                CURR_LINE_IP=$(echo $line | cut -d" " -f2)          

		# 如果这个IP链接数小于预设,终止(因为数据经过排序)
                if [ $CURR_LINE_CONN -lt $NO_OF_CONNECTIONS ]; then     
			break
                fi

		# 计算当前IP在$IGNORE_IP_LIST出现了多少次
                IGNORE_BAN=`grep -c $CURR_LINE_IP $IGNORE_IP_LIST`
		# 如果当前IP已经在$IGNORE_IP_LIST,跳过,可以通过这个方式设置永不堵塞某些IP
                if [ $IGNORE_BAN -ge 1 ]; then
			continue
                fi

		# 进入到这里表示当前必定有IP要被堵塞
                IP_BAN_NOW=1
                
		# 开始iptables封锁
                $IPT -I INPUT -s $CURR_LINE_IP -j DROP
        done < $BAD_IP_LIST

	# $IP_BAN_NOW等于1表示有IP被封锁了
        if [ $IP_BAN_NOW -eq 1 ]; then
		# 发送通知 CURL                           
        fi
fi

# 清除临时产生的文件
rm -f $TMP_PREFIX.*

如果仅仅希望监控一下哪些IP链接数过多,则可以如下改造:

#!/bin/sh

CNTS_LOG="/root/dd/log.txt"
IGNORE_IP_LIST="/root/dd/ignore.ip.list"
CONNECTIONS=5
 
TMP_PREFIX='/tmp/dd'
TMP_FILE="mktemp $TMP_PREFIX.XXXXXXXX"

BAD_IP_LIST=`$TMP_FILE`
netstat -ntu | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -nr > $BAD_IP_LIST

while read line; do
    CURR_CONN=$(echo $line | cut -d" " -f1)
    CURR_IP=$(echo $line | cut -d" " -f2)
    if [ $CURR_CONN -lt $CONNECTIONS ]; then
        break
    fi

    IGNORE=`grep -c $CURR_LINE_IP $IGNORE_IP_LIST`
    if [ $IGNORE -ge 1 ]; then
        continue
    fi

    echo "`date "+%Y-%m-%d %H:%M:%S"`|$CURR_IP|$CURR_CONN" >> $CNTS_LOG
done < $BAD_IP_LIST
 
rm -f $TMP_PREFIX.*

Linux Bash Shell编程

Bash Shell基础

1 Bash Shell

1.1 系统的合法shell与/etc/shells
Linux下可以使用的shell,可以检查一下/etc/shells这个文件,有下面几个可以用的shell:

#CentOS 7.X中的输出
cat /etc/shells

/bin/sh             #/bin/bash的链接
/bin/bash
/sbin/nologin       #用户不具备Shell环境(一般指无法登陆),指定这个值

/usr/bin/sh
/usr/bin/bash
/usr/sbin/nologin

/bin/tcsh
/bin/csh

系统某些服务在运行过程中,会去检查用户能够使用的Shell,而这些Shell的查询就是借助/etc/shells这个文件。在/etc/passwd这个文件内,每行的最后一个段指明了用户使用的shell(如果一个用户不能登录,通常是/sbin/nologin)。

1.2 Bash Shell功能
1) 命令记忆能力(history)
在用户的主文件夹内的.bash_history用来记录历史记录,但是它记录的是前一次登录以前所执行过得命令,而至于这一次登录所执行的命令都被暂存在临时内存中,当注销系统后,该命令记忆才会记录到.bash_history中。
2) 命令与文件补全功能
3) 命令别名设置功能(alias)
可以使用alias直接设置命令别名,直接输入alias可以输出当前存在的命令别名。

alias
alias cp='cp -i'
alias egrep='egrep --color=auto'
alias fgrep='fgrep --color=auto'
alias grep='grep --color=auto'
alias l.='ls -d .* --color=auto'
alias ll='ls -l --color=auto'
alias ls='ls --color=auto'
alias mv='mv -i'
alias rm='rm -i'
alias which='alias | /usr/bin/which --tty-only --read-alias --show-dot --show-tilde'

4) 作业控制、前台、后台控制(job control, foreground, background)
5) 程序脚本
6) 通配符(Wildcard)

1.3 Bash Shell的内置命令: type
为了方便Shell的操作,Bash已经内置了很多命令,利用type可以查看命令是否是内置命令。

2 Shell的变量

2.1 变量的显示与设置: echo unset
在Bash当中,当一个变量名称未被设置时,默认的内容是空的。变量的设置规则:
1) 变量与变量内容以一个等号连接,等号两边不能接空格,注意,如果有空格则是逻辑比较
2) 变量名称只能是英文字母和数字,但开头不能是数字
3) 变量内容如含有空格,需要使用单引号或双引号括起来(如没有引号括起来,但是包含了转义,最终会转义输出),但双引号内特殊字符保持本性,单引号内的任何字符都是一般文本
4) 可以使用转义符将特殊字符变成一般字符
5) 如需要引用其它命令的结果,可以使用反单引号或$()语法,比如`uname –r`和$(uname -r)等同
6) 如要为变量增加内容,可以使用$变量名或${变量名}累加内容(不需要所谓的连接符)
7) 如变量需要在其它子进程中执行,需要以export来使变量变成环境变量:export PATH。
8) 通常大写字符为系统默认变量
9) 取消变量的方法是使用unset 变量名称

2.2 环境变量

用env查看环境变量:

env

XDG_SESSION_ID=167086
HOSTNAME=xxxxx  #主机名
TERM=xterm
SHELL=/bin/bash  #使用的是哪个Shell
HISTSIZE=1000  #历史记录大小
SSH_CLIENT=113.88.168.31 17444 22
SSH_TTY=/dev/pts/3
USER=root
LS_COLORS=
MAIL=/var/spool/mail/root
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin  #执行文件查找的路径
PWD=/root
LANG=en_US.UTF-8 #语系数据,中文编码通常是zh_CN.UTF-8
HISTCONTROL=ignoredups
SHLVL=1
HOME=/root  #代表用户主文件夹
LOGNAME=root
SSH_CONNECTION=113.88.168.31 17444 120.24.42.192 22
LESSOPEN=||/usr/bin/lesspipe.sh %s
XDG_RUNTIME_DIR=/run/user/0
_=/usr/bin/env

用set查看所有变量(含环境变量与自定义变量)

PS1     提示符号设置,可以man bash查询PS1的相关说明
$       当前的Shell的PID,$也是一个变量,要引用时使用$$
?       上个命令的回传码,一般,一个命令成功执行,则会回传一个0值
OSTYPE, HOSTTYPE, MACHTYPE 主机硬件与内核的等级

自定义变量转成环境变量
子进程会继承父进程的环境变量,不会继承自定义变量。直接执行export 变量名,可以把变量转成环境变量,如果要相反操作,用declare。

2.3 影响显示结果的语系变量(locale)
可以使用locale来查询Linux支持多少语系。locale –a其实是列出/usr/lib/locale里面的文件。当设置了LANG或者是LC_ALL时,则其他的语系变量就会被这两个变量所替代。LC_ALL是整体的语系变量,它在/etc/sysconfig/i18n中设置:LANG=”zh_CN.UTF-8”。(CentOS 7.x中对应的配置文件是/etc/locale.conf文件)

2.4 变量键盘读取、数组与声明: read,array,declare
1) read

用法:
read [-pt] variable
参数:
-p 后面可以接提示符
-t 后面可以接等待秒数
 
例子:
read –p “Please putin your name:” –t 30 named
等待用户30秒输入它的名字赋值给named变量

2) declare / typeset
declare / typeset都用来声明变量的类型。

用法:
declare [-aixr] variable
参数:
-a 定义为数据
-i 定义为整数
-x 定义成环境变量
-r 定义变量为只读

默认Bash对于变量有几个基本定义:
变量类型默认为字符串,所以若不指定变量类型,则1+2为一个字符串而不是计算式
Bash环境中的数值运算,默认最多仅能到达整数,所以1/3结果是0

另,declare +x sum 把 – 换成 + 代表取消操作。一般,除非需要特别指定变量类型,否则都不需要使用declare关键字。

3) 数组变量类型
在Bash里,数组的设置方式:var[index]=content

2.5 与文件系统及程序的限制关系: ulimit
Bash是可以限制用户的某些系统资源的,包括可以打开的文件数量、可以使用的CPU时间、可以使用的内存总量等。

用法:
ulimit [-SHacdfltu] [配额]
-H hard limit
-S soft limit,可以超过,但有警告值,必须比hard小
-a 后面不接任何参数,可列出所有的限制额度
-f 此shell可以创建的最大文件容量
-d
-l 可用锁定的内存量
-t 可使用的最大CPU时间
-u 单用户可以使用最大进程数量

2.6 变量内容的删除、替代与替换

3 命令别名与历史命令

3.1 命令别名设置:alias, unalias
3.2 历史命令:history

4 Bash Shell的操作环境

4.1 路径与命令查找顺序
命令运行顺序:
1) 以相对/绝对路径执行命令,如 /bin/ls 或 ./ls (最优先)
2) 由alias找到该命令来执行
3) 由bash内置(bulitin)命令来执行
4) 通过$PATH变量的顺序找到第一个命令来执行

4.2 bash的登录与欢迎信息:/etc/issue, /etc/motd

#查看/etc/issue:
CeontOS release 5.6 (Final)
Kernel \r on an \m

#如同$PS1变量一样,issue这个文件的内容也可以使用反斜杠为变量调用:
\d  本地时间的日期
\l  显示第几个终端机接口
\m  显示硬件等级
\n  显示主机网络名称
\o  显示domain name
\r  操作系统的版本(相当于uname -r)
\t  显示本地端时间的时间
\s  操作系统名称
\v  操作系统版本

如果你想要让用户登录后取得一些信息,那么可以将信息加入/etc/motd里面,这样所有人登录后都会获取此信息。

4.3 Bash的环境配置文件

4.3.1 登录Shell 和 非登录Shell
登录Shell需要完整的登录流程,非登录Shell取得Bash接口的方法不需要重复登录的流程。

登录Shell 和 非登录Shell它们读取的配置文件数据并不一样。
登录Shell只会读取如下两个配置文件:
1) /etc/profile: 这是系统整体的设置,最好勿改动此文件
2) ~/.bash_profile 或 ~/.bash_login 或 ~/.profile: 属于用户个人设置,要改自己的数据就放这里。

4.3.2 /etc/profile
这个配置文件可以利用用户的标示符UID来决定很多重要的变量数据。每个用户登录取得Bash时一定会读取的配置文件。这个文件设置的变量主要有:PATH, MAIL, USER, HOSTNAME, HISTSIZE。 除此还调用外部数据:
1) /etc/inputrc
/etc/profile会主动判读用户有没有自定输入的按键功能,如果没有,它就会决定设置”INPUTRC=/etc/inputrc”这个变量。此文件内容为Bash的热键等数据。
2 /etc/profile.d/*.sh
这个目录下面的文件规定了Bash操作接口的颜色、语系、ll与ls命令的命令别名、vi的命令别名、which的命令别名等。如果需要帮所有用户设置一些共享的命名别名时,可以在这个目录下面创建.sh的文件,并写入数据。

4.3.3 /etc/sysconfig/i18n
这个文件是由/etc/profile.d/lang.sh调用的。bash默认使用何种语系的配置文件。

4.3.4 ~/.bash_profile
~/.bash_profile 或 ~/.bash_login 或 ~/.profile从左到右,只会读取其中一个。查看~/.bash_profile发现,它其实还运行了. ~/.bashrc

在CentOS的登录Shell环境下,最终被读取的配置文件是”~/.bashrc”这个文件。所以可以将自己的偏好设置写入该文件。

4.3.5 source: 读入环境配置文件的命令

用法:
source 配置文件名
例子:source ~/.bashrc 和 . ~/.bashrc一样

利用source或小数点都可以将配置文件的内容读进当前的shell环境中。

4.3.6 ~/.bashrc
非登录Shell仅会读取~/.bashrc。查看其内容发现它会主动调用/etc/bashrc,而它定义如下数据:
依据不同的UID规定umask的值和提示符(PS1)
调用/etc/profile.d/*.sh的设置

4.3.7 其它相关配置文件
/etc/man.conf
规定了使用man的时候man page的路径到哪里去寻找。
~/.bash_history
~/.bash_logout
当注销bash后系统再所完什么操作后才离开。

4.4 终端机的环境设置:stty, set

4.5 通配符与特殊符号

*       0个或多个
?       任意一个字符
[]      在其中的一个字符
[-]     如[a-z] [0-9]
[^]     排除之中的字符的一个字任意字符

#除了通配符,bash中的特殊符号:
#       注释
\       转移符号
|       管道
;       分隔符号
~       用户的主文件夹
$       变量引用符
&       后台运行
!       逻辑运算非
/       目录符
>, >>   数据流重定向,输出导向

5 数据流重定向

执行一个命令的时候,这个命令可能会由文件读入数据,经过处理之后,再将数据输出到屏幕上。标准输出与标准错误输出这两个命令默认都是输出到屏幕上面来。标准输出指的是命令执行所回传的正确的信息,而标准错误输出可理解为命令执行失败后,所回传的错误信息。

数据流重定向可以将stdout与stderr分别传送到其它的文件或设备去,所用到的特殊字符:
1) 标准输入(stdin): 代码为0,使用< 或 <<(指定结束输入的定界符); [bash] cat > catfile << "eof" [/bash] cat从标准输入接收输入然后显示,这里定位到catfile中,输入遇到定界符eof即刻结束输入。 2) 标准输出(stdout):代码为1,使用> 或 >>
3) 标准错误输出(stderr): 代码为2,使用2> 或 2>>

/dev/null垃圾桶设备与特殊写法
丢弃错误信息
2> /dev/null
如果所有信息都丢弃或都写入一个文件
> /dev/null 2>&1 或 &> /dev/null 此为特殊写法

标准输入: < 与<<
将原本需要有键盘输入的数据改由文件内容来替代。

命令执行的判断依据(; , && ||)
使用逗号可以在一行中连续执行多个命令
每个命令执行都有回传码,成功执行回传0,失败回传1,bash使用$?在动态记录这个这个值(?是一个变量,$也是变量,表示Bash的进程,使用$$来引用)。&& 和 ||可以用来分隔两个命令,使用&&时表示第一个命令执行成功则执行第二个,||表示第一个执行失败则执行第二个(判断依据是当前bash的$?的记录,所以当有多个&& 和 ||时需要注意,它判断的依据不是前面的命令,而是当前的bash记录的$?,当然,命令的执行会影响到$?值)

6 管道命令(pipe)

管道命令使用 | 界定符号。管道命令 | 仅能处理经由前面一个命令传来的正确信息,也就是标准输出的信息,对于标准错误输出并没有直接处理的能力。

在每个管道后面接的第一个数据必定是命令,而且这个命令必须要能接收标准输入的数据才行,这样的命令才可以是管道命令,管道命令有两个要注意的地方:
管道命令仅会处理标准输出,对于标准错误输出会予以忽略
管道命令必须要能够接收来自前一个命令的数据成为标准输入继续处理才行。

6.1 选取命令: cut, grep
一般,选取信息通常是针对行来分析的,并不是整篇信息分析的。

cut
主要用途在于将同一行里面的数据进行分解。

用法:
cut –d ‘分隔符’ -f fields
cut –c 字符范围
参数:
-d 后面接分隔字符,与-f 一起使用
-f 依据-d的分隔字符将一段信息切割为数段,用-f 取出第几段的意思
-c 以字符的单位取出固定字段区间
 
例子:
echo $PATH | cut -d ‘:’ -f 5 用:分割,取出第5段
echo $PATH | cut –d ‘:’ -f 3,5 用:分割,取出第3和第5段

cut在处理多空格相连的数据时比较困难。

grep
分析一行信息,如果符合条件则把整行信息输出。

用法:
grep [-acinv] [--color=auto] ‘查找字符串’ filename
参数:
-a 将binary文件以text文件的方式查找书籍
-c 计算找到’查找字符串’的次数
-i 忽略大小写
-n 输出行号
-v 反向选择,即显示出不符合的行
--color=auto 将找到的关键字部分加上颜色显示
 
例子:
last | grep ‘root’ | cut –d ‘ ’ -f 1
grep -n "conf" /etc/*  #找出文件含有conf字符串的行(文件)

6.2 排序命令:sort, wc, uniq

sort
对行进行排序,排序的字符与语系的编码有关,建议使用LANG=C来让语系统一。

用法:
sort [-fbMnrtuk] [file or stdin]
参数:
-f 忽略大小写
-b 忽略最前面的空格符
-M 以月份的名字来排序
-n 使用纯数字进行排序(默认以文字类型来排序)
-r 反向排序
-u 就是uniq,去重复
-t 分割符号,默认是用[Tab]来分隔
-k 以哪个区间来进行排序
 
例子:
cat /etc/passwd | sort -t ':' -k 3 –n
用冒号分隔取第三段当做纯数字来排序(-n 表示纯数字)

uniq
排序完成,将重复的行去掉。

用法:
uniq [-ic]
参数:
-i 忽略大小写
-c 进行计数
 
例子:
last | cut -d ‘ ‘ -f 1 | sort | uniq

wc

用法:
wc [-lwm]
参数:
-l 行数
-w 字数
-m 字符数

6.3 双向重定向: tee
tee会同时将数据流送到文件与屏幕,而输出到屏幕的,其实就是stdout,可以让下个命令继续处理。

用法:
tee [-a] file
参数:
-a 以累加(append)的方式将数据加入file当中
 
例子:
last | tee last.list | cut -d “ ” -f 1
利用tee保存一份数据到last.list并输出到屏幕。

6.4 字符转换命令:tr,col,join,paste,expand

tr

tr [-ds] SET1
参数:
-d 删除信息当中的SET1这个字符串
-s 替换掉重复的字符
 
例子:
last | tr ‘[a-z]’ ‘[A-Z]’
cat /root/passwd | tr –d ‘\r’

col

col [-xb]
参数:
-x 将tab键转换成对等的空格键
-b 在文字内有反斜杠(/)时,仅保留反斜杠最后接的那个字符

join

join [-ti12] file1 file2
参数:
-t join默认以空格符分割数据,并且对比第一个字段的数据,如两个文件相同,则将两条数据练成一行,且第一个字段放在第一个,-t指定分割符号
-i 忽略大小写
-1 数字1,代表第一个文件用药哪个字段来分析的意思
-2代表第二个文件用药哪个字段来分析
 
/etc/passwd的第四个字段是GID,哪个GID记录在/etc/group当中的第三个字段
join -t ‘:’ -1 4 /etc/passwd -2 3 /etc/group

相当关系数据库中的关联引用。注:在使用join之前,你所需要处理的文件应该要事先经过排序处理。

paste
直接将两行贴在一起,且中间以[tab]键隔开。

paste [-d] file1 file2
参数:
-d 后面可以接分隔符,模式是[tab]
- 如果file部分写成-,表示来自标准输入的数据
 
例子:
paste /etc/passed /etc/shadow
cat /etc/group | paste /etc/passwd /etc/shadow - | head –n 3
里面的横杆(-)表示来自cat的输出

expand
将[tab]按键转成空格键。

expand [-t] file
参数:
-t 后面可以接数字,一般,一个[tab]按键可以用8个空格键替换,也可以自行定义一个[tab]按键代表多少个字符。

6.5 切割命令:split
将大文件依据文件大小或行数来切割成小文件。

split [-bl] file PREFIX
参数:
-b 后接要切割成的文件大小,单位是b, k, m等
-l 以行数进行切割
PREFIX 代表前导符,可以作为切割文件的前导文字

6.6 参数代换:xargs
xargs再产生某个命令的参数。xargs可以读入stdin的数据,并且以空格符或断行字符进行分辨,将stdin的数据分割成arguments。使用xargs的原因是很多命令其实并不支持管道命令,因此可以通过xargs来提供该命令引用标准输入之用。

用法:
xargs [-0epn] command
参数:
-0 如果stdin含有特殊字符,这个参数可以将它还原成一般字符。
-e 这个是EOF意思。后面可以接一个字符串,当xargs分析道这个字符串时,就会停止工作。
-p 在执行每个命令的参数时,都会询问用户的意思
-n 后面接次数,每次command命令执行时,要使用几个参数的意思
 
例子:
cut –d ‘:’ –f1 /etc/passwd | xargs –p –n 5 finger
这样finger将从前面获取5个参数。
 
find /sbin –perm +7000 | xargs ls –l
通过xargs把前面的输出作为后面命令的参数,这个就是让ls原本不支持管道的命令间接支持管道的用法。

6.7 关于减号 – 的用途
在管道命令当中,经常会使用到前一个命令的stdout作为这次的stdin, 某些命令需要用到文件名来进行处理时,该stdin与stdout可以利用减号来替代(经过管道后前面的stdout就是后面的stdin,可以用-来引用前面的stdout或叫当前命令的stdin)。

tar –cvf – /home | tar –xvf –
前面把/home压缩到stdout,被管道带到下一个命令,下个命令使用减号引用管道带过来的前一个命令的stdout,作为其输入,然后再接压缩到stdout。

Bash Shell 正则表达式
1 基础正则表达式
1.1 语系对正则表达式的影响
使用正则表达式时,需要留意当时环境的语系,否则结果可能有差异。可以使用统一语系,比如LANG=C

[:alnum:]       英文大小写字符和数字,即0-9,a-z, A-Z
[:alpha:]       英文大小写字符,即a-z, A-Z
[:blank:]       空格和[Tab]
[:cntrl:]       键盘上控制键,即包括CR, LF, Tab, Del等
[:digit:]       数字,即0-9
[:graph:]       除了空格键以为所有按键
[:lower:]       小写字母
[:print:]       可以被打印的字符
[:punct:]       标点符号
[:upper:]       大写字母
[:space:]       空格
[:xdigit:]      十六进制的数字类型

1.2 基础正则表达式字符

^word        以word开头
word$        以word结尾
.        一个任意字符
\        转义字符
*        重复任意次
[list]       选取列出来的其中一个字符
[n1-n2]      选择n1到n2之间的一个字符,比如[0-9]
[^list]      选取排除了列出来的字符的其它任一字符
\{n,m\}      重复n次,最多不超m次,如果是\{n \}则表示重复n次,如果是\{n,\}则上重复n次以上

1.3 扩展正则表达式

+       重复一次或一次以上
?       重复零次或一次
|       用或的方式找出数个字符串
()      分组
()+     重复组

Bash Shell 工具- sed

rpm -qa | grep sed
sed-4.2.2-5.el7.x86_64

工具sed并不是Bash Shell的组成部分(grep cut等这些是内置命令),不过这个工具是Bash Shell编程不缺少的部分。

sed本身是一个管道命令,可以分析标准输入,而且sed还可以将数据进行替换、删除、新增、选取特定行等。

用法:
sed [-nefr] [动作]
参数:
-n 使用安静模式,一般,所有来自STDIN的数据都会列出到屏幕上,加入-n后,只有经过sed处理的那行才会被列出
-e 直接在命令行模式上进行sed的动作编辑
-f 直接将sed的动作写在一个文件内,-f filename则可以执行filename内的sed动作
-r sed的动作支持的是扩展正则语法
-i 直接修改读取的文件内容
 
动作说明:
[n1,[,n2]]function
n1, n2 选择进行动作的行数
 
function 参数
a 新增,a后可以接字符串
c 替换
d 删除
i  插入
p 打印
s 替换 通常可以搭配正则,如1,20s/old/new/g

例子:
1) 以行为单位的新增/删除
nl /etc/passwd | sed ‘2,5d’
将2~5行删除,d表示删除,sed后面接的动作务必用单引号括住,应该应该是sed –e,不用也可以。如果要删除3行以后所有行,nl /etc/passwd | sed ‘3,$d’,这个$表示最后一行,如果只要删除第二行,则nl /etc/passwd | sed ‘2d’

nl /etc/passwd | sed ‘2a vfeelit’
在第二行后加上vfeelit字符串,如果把a改成i,则是在第二行之前插入,如果要增加多行,每行后面需要使用转义字符分隔。

2) 以行为单位的替换与显示
nl /etc/passwd | sed ‘2,5c vfeelit’
这里的c表示替换,2到5行用给定的字符替换。

nl /etc/passwd | sed –n ‘5,7p’
这里的-n表示只有经过处理的打印出来(p),sed是把所有的都进行输出的,比如要列出11-20行,没有sed工具之前需要这样:head –n 20 | tail –n 10

3) 部分数据的查找并替换
sed的查找与替换与vi相当类似:
sed ‘s/查找字符串/新字符串/g’ 字母s前可以限定行数,比如2,10

cat /etc/man.config | grep ‘MAN’ | sed ‘s/#.*$//g’ | sed ‘/^$/d’

4) 直接修改文件内容
sed –s ‘s/\.$/\!/g’ regular.txt
直接在regular.txt中修改最后字符为.的修改为!

sed的”-i”参数可以直接修改文件内容。

Bash Shell 工具- awk

rpm -qa | grep awk
gawk-4.0.2-4.el7.x86_64

awk主要是处理每一行的字段内的数据,而默认的字段的分隔符为空格或[tab]键。

用法:
awk ‘条件类型1 {动作1} 条件类型2 {动作2} ..’ filename

注:注意其严格的格式,awk后接单引号,动作需要用大括号括起来。
例子:
last –n 5 | awk ‘{print $1 “\t” $3}’
显示每一行的第一和第三字段。

每一行的每个字段都是有变量名称的,那就是$1,$2 … 等变量名称。还有$0,表示整行数据的意思。

整个awk处理流程:
-读入一行,将数据填入$0,$1,$2这些变量中
-依据条件类型,判断是否执行后面的动作
-做完所有条件和动作
-如果还有后续行,重复

awk以行为单位,以字段为最小处理单位。至于数据有几行几列,则需要awk内置变量的支持:

NF      每一行拥有的字段总算
NR      带便目前处理的是第几行
FS      目前的分隔字符,默认是空格键

例子:
last –n 5 | awk ‘{print $1 “\t lines: “ NR “\t coleus:” NF}’

awk的逻辑运算符跟一般的没有差别,==表示判断是否等于,赋值使用=
例子:

cat /etc/passwd | awk ‘{FS=”:”} $3<10 {print $1 “\t ” $3}’

这里第一行没有正确显示,因为读入第一行时,那些$1,$2变量还是以空格作为分隔的,虽然用来FS=”:”,但是是从第二行开始生效的。可以使用BEGIN这个关键字预先设置awk的变量。

cat /etc/passwd | awk ‘BEGIN {FS=”:”} $3<10 {print $1 “\t ” $3}’

几个重要说明:
所有awk的动作,即在{}内的动作,如果有需要多个命令辅助时,可利用分号分隔
逻辑运行如果是等于判断务必使用==
格式化输出时,在printf的格式设置当中,务必加上\n才能进行分行
与bash shell变量不同,在awk中,变量可以直接使用,不需要加上$符号

Bash Shell 脚本
1 Shell Script入门
Shell Script编写注意事项:
1) 命令的执行从上而下,从左而右地分析与执行
2) 命令、参数间的多个空白会被忽略,空白行也被忽略,[tab]按键被视为空格键
3) 如果读取到一个Enter符号(CR),就尝试开始执行命令(以行为分隔)
4) #符号为注释

执行Shell Script方法:
1) 直接执行:Shell Script文件必须要具备可读可执行的权限,然后:
使用绝对路径来执行 比如/root/shell.sh
使用相对路径来执行 比如./shell.sh
变量PATH功能,将shell.sh放在PATH指定的目录内,如/bin/,然后让shell自动搜索PATH获取这个脚本并执行

2) 以bash进程来执行:通过bash shell.sh 或sh shell.sh来执行
如果shell.sh在/bin内具有rx的权限,那就直接输入shell.sh即可执行该脚本程序。因为/bin/sh是/bin/bash的链接文件,使用sh就是间接使用bash,此时shell.sh只要有r的权限即可被执行。

Shell Script组成部分
1) 第一行#!/bin/bash 声明这个script使用的shell名称,这样就能根据声明加载Shell(这里是bash)的相关环境配置文件(一般来说就是非登录Shell的~/.bashr)
2) 程序说明部分,以#开头,添加必要的说明信息
3) 主要环境变量声明
4) 程序部分
5) 告知执行结果
一个命令的执行是否成功,可以使用$?这个变量来查看。那么也可以利用exit这个命令来让程序中断,并且回传一个数值给系统。比如exit 0,这代表离开Script并且回传一个0给系统,如果执行echo $?则可得到0的值。利用exit n (n是数字)的功能,可以自定义错误信息。

范例:
1) 交互式脚本:变量内容由用户决定

#!/bin/bash
read –p “Please input you full name: ” fullname #提示用户输入
echo –e “\n Your full name is: $fullname”

2) 数值运算:简单的加减乘除
用declare来把变量定义成整数后才能进行加减运行,此外还可以利用$((计算式))来进行数值运算。Bash Shell里面仅支持整数。

#!/bin/bash
#
firstnu=10
secnu=6
total=$(($firstnu*$secnu))
echo –e “\n The result is: $total”

在数值运行上,也可以使用declare –i total=$firestnu*$secnu。

Shell Script的执行方式区别
脚本的执行除了以上提到的方式还可以利用source或小数点来执行。

1) 利用直接执行的方式来执行
直接命令执行(绝对路基、相对路径或$PATH内),或利用bash/sh来执行脚本时,该脚本都会使用一个新的bash环境来执行脚本内的命令。当子进程完成后,子进程内的各项变量或操作将会结束而不会传回到父进程中。
2) 利用source来执行脚本:在父进程中执行

2 判断式
2.1 利用test命令的测试功能
以下只列常用:

关于某个文件的“文件类型”判断,如test –e feliename表示是否存在

-e 		该文件是否存在
-f		该文件是否存在并且是文件
-d		该文件是否存在并且是目录

关于文件权限的检测,如test –r filename表示是否可读(但root除外)

-r		检测文件是否存在并且可读
-w 		检测文件是否存在并且可写
-x 		检测文件是否存在并且可执行
-u 		检测文件是否存在并且具有SUID属性
-g 		检测文件是否存在并且具有SGID属性
-k 		检测文件是否存在并且具有Sticky bit属性

关于两个整数之间的判断,如test n1 –eq n2

-eq		两数值相等 equal
-ne		两数值不等 not equal
-gt		n1大于n2 greater than
-lt		n1小于n2 less than
-ge		n1大于等于n2 grater than or equal
-le		n1小于等于n2 less than or equal

判定字符串的数据

test –z string	判定字符串是否为0,若string为空字符串,则为true
test –n string	判定字符串是否为非0,若string为空字符串,则为false
test str1=str2	判定str1是否等于str2,若相等,则回传ture
test str1!=str2	判定str1是否不等于str2,若相等,则回传false

多重条件判定,如test –r filename –a –x filename

-a		两个条件同时成立
-o		其中一个条件成立
!		取反,如test ! –x file,当file不具有x时,回传true

2.2 利用判断符号[]
可以使用[]来替代test,如[ -z “$HOME” ]; echo $?
注:在bash中用中括号作为shell的判断式时,必须要注意中括号的两端需要有空格符来分隔,如[空格”$HOME”空格==空格”$MAIL”空格]

2.3 Shell Script的默认变量($0,$1….)

Script针对参数已经设置好一些变量名称:

/path/to/scriptname  opt1  opt2  opt3
$0	             $1    $2    $3

执行的脚本文件名为$0变量,第一个接的参数就是$1。除此,还有一些特殊变量:

$#		代表后接的参数个数
$@		代表”$1”、 ”$2”、 ”$3”之意,每个变量是独立的(用双引号括起来)
$* 		代表”$1c$2c$3c$4”,其中c为分隔字符,默认为空格键

shift:造成参数变量号码偏移(**)
shift会移动变量,而且shift后面可以接数字,代表拿掉最前面几个参数的意思。

3 条件判断式
3.1 利用if..then
单层、简单条件判断式

if [ 条件判断式 ]; then
    内容
fi

如果有多个条件要判断时,可以有多个中括号来隔开,括号之间以&&或||隔开:
&&代表AND, ||代表or
比如:[ “$yn” == ”Y” –o “$yn” == ”y” ] 可以替代为:[ “$yn” == ”Y” ] || [ “$yn” == ”y” ]

多重、复杂条件判断式

if [ 条件判断式 ]; then
else
fi
 
if [ 条件判断式一 ]; then
elif [ 条件判断式二 ]; then
else
fi

elif也是个判断式,因此出现elif后面都要接then来处理。

3.2 利用case..esac判断
语法:

case $变量名称 in
    “第一个变量内容”)
        程序段
        ;;
    “第二个变量内容”)
        程序段
        ;;
    *)                  #最后一个变量内容都会用*来代表所有权其它值
        程序段
        exit 1
        ;;
esac

3.3 利用funtion功能
函数可以在Shell Script当中做出一个类似自定义执行命令的东西,因为Shell Script的执行方式是由上而下、由左而右的,因此在Shell Script当中的function的设置一定要在程序的最前面。

另外,function也是拥有内置变量的,它的内置变量与Shell Script很类似,函数名称代表$0,而后续接的变量也是以$1$2..来替代的(如果要向函数传递参数,对应位置的参数将替换$1$2..,比如一个函数叫getList,传递参数格式: getList aaa bbb, 这个函数内$1等于aaa,$2等于bbb)。

4 循环
4.1 while do done, until do done (不定循环)

while [ condition ]
do
    程序段
done
 
until [ condition ]
do
        程序段
done

#计算1+2+3+…+100
#!/bin/bash
#
s=0;
i=0
while [ “$i” != “100” ]
do
    i=$(($i+1))
    s=$(($s+$i))
done
echo “The result is: $s”

4.2 for..do..done(固定循环)

for var in con1 con2 con3 …
do
    程序段
done

4.3 for…do…done的数值处理

for ((初始值; 限制值; 执行步长))
do
    程序段
done

###
for ((i=1; i<=$nu; i=i+1))
do
    s=$(($s+$i))
done

4.4 Shell Script的追踪与调试

用法;
sh [-nvx] script.sh
参数:
-n 不要执行script,仅检查语法
-v 在执行script前先将其内容输出到屏幕上
-x 将使用到的script内容显示到屏幕上

由sh –x的方式来将命令执行过程也显示出来,如此用户可以判断程序代码执行到哪一段时会出现相关信息。