├── 9781430258544.jpg ├── LICENSE.txt ├── README.md ├── contributing.md └── source ├── README.md ├── chapter01 ├── README.md ├── getname.py ├── search1.py ├── search2.py ├── search3.py ├── search4.py ├── search4.txt └── stringcodes.py ├── chapter02 ├── README.md ├── big_sender.py ├── udp_broadcast.py ├── udp_local.py └── udp_remote.py ├── chapter03 ├── README.md ├── tcp_deadlock.py └── tcp_sixteen.py ├── chapter04 ├── README.md ├── dns_basic.py ├── dns_mx.py └── www_ping.py ├── chapter05 ├── README.md ├── blocks.py ├── examples.rst └── streamer.py ├── chapter06 ├── README.md ├── ca.crt ├── features.py ├── localhost.pem ├── safe_tls.py └── test_tls.py ├── chapter07 ├── README.md ├── client.py ├── in_zen1.py ├── in_zen2.py ├── inetd.conf ├── srv_async.py ├── srv_asyncio1.py ├── srv_asyncio2.py ├── srv_legacy1.py ├── srv_legacy2.py ├── srv_single.py ├── srv_threaded.py ├── test.sh ├── trace.out └── zen_utils.py ├── chapter08 ├── README.md ├── hashing.py ├── queuecrazy.py ├── queuepi.py └── squares.py ├── chapter09 ├── README.md ├── config.py └── examples.doctest ├── chapter10 ├── README.md ├── _test.py ├── timeapp_raw.py ├── timeapp_webob.py ├── timeapp_werkz.py └── wsgi_env.py ├── chapter11 ├── README.md ├── app_improved.py ├── app_insecure.py ├── attack.js ├── bank.py ├── csrf.html ├── csrf_auto.html ├── djbank │ ├── __init__.py │ ├── admin.py │ ├── fixtures │ │ └── start.json │ ├── models.py │ ├── settings.py │ ├── templates │ │ ├── base.html │ │ ├── index.html │ │ ├── pay.html │ │ └── registration │ │ │ └── login.html │ ├── urls.py │ ├── views.py │ └── wsgi.py ├── manage.py ├── mscrape.py ├── rscrape1.py ├── rscrape2.py ├── static │ └── style.css ├── templates │ ├── base.html │ ├── index.html │ ├── login.html │ ├── pay.html │ └── pay2.html └── tinysite │ ├── further.html │ ├── index.html │ ├── page1.html │ ├── page2.html │ ├── page3.html │ ├── page4.html │ ├── page5.html │ ├── page6.html │ └── search.html ├── chapter12 ├── README.md ├── attachment.gz ├── attachment.txt ├── build_basic_email.py ├── build_mime_email.py ├── build_unicode_email.py ├── display_email.py ├── display_structure.py └── pre-python-3.4 │ ├── README.rst │ ├── mime_decode.py │ ├── mime_gen_alt.py │ ├── mime_gen_basic.py │ ├── mime_gen_both.py │ ├── mime_headers.py │ ├── mime_parse_headers.py │ ├── mime_structure.py │ ├── trad_gen_newhdrs.py │ ├── trad_gen_simple.py │ └── trad_parse.py ├── chapter13 ├── README.md ├── debug.py ├── ehlo.py ├── login.py ├── simple.py └── tls.py ├── chapter14 ├── README.md ├── apopconn.py ├── download-and-delete.py ├── mailbox.py └── popconn.py ├── chapter15 ├── README.md ├── folder_info.py ├── folder_summary.py ├── open_imap.py ├── open_imaplib.py └── simple_client.py ├── chapter16 ├── README.md ├── sftp_get.py ├── shell.py ├── ssh_commands.py ├── ssh_simple.py ├── ssh_threads.py ├── telnet_codes.py └── telnet_login.py ├── chapter17 ├── README.md ├── advbinarydl.py ├── advbinaryul.py ├── asciidl.py ├── binarydl.py ├── binaryul.py ├── connect.py ├── dir.py ├── nlst.py └── recursedl.py ├── chapter18 ├── README.md ├── jsonrpc_client.py ├── jsonrpc_server.py ├── rpyc_client.py ├── rpyc_server.py ├── testfile.txt ├── xmlrpc_client.py ├── xmlrpc_introspect.py ├── xmlrpc_multicall.py └── xmlrpc_server.py ├── requirements.txt ├── session.txt └── tools ├── monkeys └── _bootlocale.py ├── run.sh └── two.sh /9781430258544.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/foundations-of-python-network-programming-14/742320235e7a62854b7cbb4b21a4b1b3c6e07c08/9781430258544.jpg -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/foundations-of-python-network-programming-14/742320235e7a62854b7cbb4b21a4b1b3c6e07c08/LICENSE.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apress Source Code 2 | 3 | This repository accompanies [*Foundations of Python Network Programming*](http://www.apress.com/9781430258544) by Brandon Rhodes and John Goerzen (Apress, 2014). 4 | 5 | ![Cover image](9781430258544.jpg) 6 | 7 | Download the files as a zip using the green button, or clone the repository to your machine using Git. 8 | 9 | ## Releases 10 | 11 | Release v1.0 corresponds to the code in the published book, without corrections or updates. 12 | 13 | ## Contributions 14 | 15 | See the file Contributing.md for more information on how you can contribute to this repository. 16 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to Apress Source Code 2 | 3 | Copyright for Apress source code belongs to the author(s). However, under fair use you are encouraged to fork and contribute minor corrections and updates for the benefit of the author(s) and other readers. 4 | 5 | ## How to Contribute 6 | 7 | 1. Make sure you have a GitHub account. 8 | 2. Fork the repository for the relevant book. 9 | 3. Create a new branch on which to make your change, e.g. 10 | `git checkout -b my_code_contribution` 11 | 4. Commit your change. Include a commit message describing the correction. Please note that if your commit message is not clear, the correction will not be accepted. 12 | 5. Submit a pull request. 13 | 14 | Thank you for your contribution! -------------------------------------------------------------------------------- /source/README.md: -------------------------------------------------------------------------------- 1 | 2 | Underneath this `py3` directory is a folder for each chapter of the 3 | recent Third Edition of *Foundations of Python Network Programming* from 4 | Apress. To learn more, move one level up in this directory tree to read 5 | the [Table of Contents](https://github.com/brandon-rhodes/fopnp). 6 | -------------------------------------------------------------------------------- /source/chapter01/README.md: -------------------------------------------------------------------------------- 1 | [Return to the Table of Contents](https://github.com/brandon-rhodes/fopnp#readme) 2 | 3 | # Chapter 1
Introduction to Client-Server Networking 4 | 5 | This is a directory of program listings from Chapter 1 of the book: 6 | 7 |
8 |
Foundations of Python Network Programming
9 |
10 | Third Edition, October 2014
11 | by Brandon Rhodes and John Goerzen 12 |
13 |
14 | 15 | You can learn more about the book by visiting the 16 | [root of this GitHub source code repository](https://github.com/brandon-rhodes/fopnp#readme). 17 | 18 | All the scripts in this chapter have been tested successfully under 19 | Python 2. Simply use [3to2](https://pypi.python.org/pypi/3to2) to 20 | convert them to the older syntax. 21 | 22 | The four scripts in this chapter with “search” in their name perform 23 | exactly the same Google geocoding query, but at four different levels of 24 | abstraction in the network protocol hierarchy. This lets the chapter 25 | launch an introduction of each level that the book will be discussing. 26 | 27 | ``` 28 | $ python search1.py 29 | (41.521954, -84.306691) 30 | ``` 31 | 32 | ``` 33 | $ python search2.py 34 | {'lat': 41.521954, 'lng': -84.306691} 35 | ``` 36 | 37 | ``` 38 | $ python search3.py 39 | {'lat': 41.521954, 'lng': -84.306691} 40 | ``` 41 | 42 | ``` 43 | $ python search4.py 44 | HTTP/1.1 200 OK 45 | Content-Type: application/json; charset=UTF-8 46 | Date: Tue, 21 Oct 2014 22:50:21 GMT 47 | Expires: Wed, 22 Oct 2014 22:50:21 GMT 48 | Cache-Control: public, max-age=86400 49 | Vary: Accept-Language 50 | Access-Control-Allow-Origin: * 51 | Server: mafe 52 | X-XSS-Protection: 1; mode=block 53 | X-Frame-Options: SAMEORIGIN 54 | Alternate-Protocol: 80:quic,p=0.01 55 | Connection: close 56 | 57 | { 58 | "results" : [ 59 | { 60 | "address_components" : [ 61 | { 62 | "long_name" : "207", 63 | "short_name" : "207", 64 | "types" : [ "street_number" ] 65 | }, 66 | { 67 | "long_name" : "North Defiance Street", 68 | "short_name" : "N Defiance St", 69 | "types" : [ "route" ] 70 | }, 71 | { 72 | "long_name" : "Archbold", 73 | "short_name" : "Archbold", 74 | "types" : [ "locality", "political" ] 75 | }, 76 | { 77 | "long_name" : "German", 78 | "short_name" : "German", 79 | "types" : [ "administrative_area_level_3", "political" ] 80 | }, 81 | { 82 | "long_name" : "Fulton County", 83 | "short_name" : "Fulton County", 84 | "types" : [ "administrative_area_level_2", "political" ] 85 | }, 86 | { 87 | "long_name" : "Ohio", 88 | "short_name" : "OH", 89 | "types" : [ "administrative_area_level_1", "political" ] 90 | }, 91 | { 92 | "long_name" : "United States", 93 | "short_name" : "US", 94 | "types" : [ "country", "political" ] 95 | }, 96 | { 97 | "long_name" : "43502", 98 | "short_name" : "43502", 99 | "types" : [ "postal_code" ] 100 | } 101 | ], 102 | "formatted_address" : "207 North Defiance Street, Archbold, OH 43502, USA", 103 | "geometry" : { 104 | "location" : { 105 | "lat" : 41.521954, 106 | "lng" : -84.306691 107 | }, 108 | "location_type" : "ROOFTOP", 109 | "viewport" : { 110 | "northeast" : { 111 | "lat" : 41.5233029802915, 112 | "lng" : -84.3053420197085 113 | }, 114 | "southwest" : { 115 | "lat" : 41.5206050197085, 116 | "lng" : -84.30803998029151 117 | } 118 | } 119 | }, 120 | "types" : [ "street_address" ] 121 | } 122 | ], 123 | "status" : "OK" 124 | } 125 | 126 | ``` 127 | 128 | The remaining two scripts are quite tiny. The first shows how a 129 | hostname is turned into an IP address, and the second illustrates the 130 | basic string decoding and encoding maneuvers that Python 3 is careful to 131 | require of network programmers. 132 | 133 | ``` 134 | $ python getname.py 135 | The IP address of maps.google.com is 74.125.228.105 136 | ``` 137 | 138 | ``` 139 | $ python stringcodes.py && cat eagle.txt 140 | '413 is in.' 141 | We copy you down, Eagle. 142 | ``` 143 | -------------------------------------------------------------------------------- /source/chapter01/getname.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter01/getname.py 4 | 5 | import socket 6 | 7 | if __name__ == '__main__': 8 | hostname = 'maps.google.com' 9 | addr = socket.gethostbyname(hostname) 10 | print('The IP address of {} is {}'.format(hostname, addr)) 11 | -------------------------------------------------------------------------------- /source/chapter01/search1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter01/search1.py 4 | 5 | from pygeocoder import Geocoder 6 | 7 | if __name__ == '__main__': 8 | address = '207 N. Defiance St, Archbold, OH' 9 | print(Geocoder.geocode(address)[0].coordinates) 10 | -------------------------------------------------------------------------------- /source/chapter01/search2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter01/search2.py 4 | 5 | import requests 6 | 7 | def geocode(address): 8 | parameters = {'address': address, 'sensor': 'false'} 9 | base = 'http://maps.googleapis.com/maps/api/geocode/json' 10 | response = requests.get(base, params=parameters) 11 | answer = response.json() 12 | print(answer['results'][0]['geometry']['location']) 13 | 14 | if __name__ == '__main__': 15 | geocode('207 N. Defiance St, Archbold, OH') 16 | -------------------------------------------------------------------------------- /source/chapter01/search3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter01/search3.py 4 | 5 | import http.client 6 | import json 7 | from urllib.parse import quote_plus 8 | 9 | base = '/maps/api/geocode/json' 10 | 11 | def geocode(address): 12 | path = '{}?address={}&sensor=false'.format(base, quote_plus(address)) 13 | connection = http.client.HTTPConnection('maps.google.com') 14 | connection.request('GET', path) 15 | rawreply = connection.getresponse().read() 16 | reply = json.loads(rawreply.decode('utf-8')) 17 | print(reply['results'][0]['geometry']['location']) 18 | 19 | if __name__ == '__main__': 20 | geocode('207 N. Defiance St, Archbold, OH') 21 | -------------------------------------------------------------------------------- /source/chapter01/search4.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter01/search4.py 4 | 5 | import socket 6 | from urllib.parse import quote_plus 7 | 8 | request_text = """\ 9 | GET /maps/api/geocode/json?address={}&sensor=false HTTP/1.1\r\n\ 10 | Host: maps.google.com:80\r\n\ 11 | User-Agent: search4.py (Foundations of Python Network Programming)\r\n\ 12 | Connection: close\r\n\ 13 | \r\n\ 14 | """ 15 | 16 | def geocode(address): 17 | sock = socket.socket() 18 | sock.connect(('maps.google.com', 80)) 19 | request = request_text.format(quote_plus(address)) 20 | sock.sendall(request.encode('ascii')) 21 | raw_reply = b'' 22 | while True: 23 | more = sock.recv(4096) 24 | if not more: 25 | break 26 | raw_reply += more 27 | print(raw_reply.decode('utf-8')) 28 | 29 | if __name__ == '__main__': 30 | geocode('207 N. Defiance St, Archbold, OH') 31 | -------------------------------------------------------------------------------- /source/chapter01/search4.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Content-Type: text/javascript; charset=UTF-8 3 | Vary: Accept-Language 4 | Date: Wed, 21 Jul 2010 16:10:38 GMT 5 | Server: mafe 6 | Cache-Control: private, x-gzip-ok="" 7 | X-XSS-Protection: 1; mode=block 8 | Connection: close 9 | 10 | { 11 | "name": "207 N. Defiance St, Archbold, OH", 12 | "Status": { 13 | "code": 200, 14 | "request": "geocode" 15 | }, 16 | "Placemark": [ { 17 | ... 18 | "Point": { 19 | "coordinates": [ -84.3063479, 41.5228242, 0 ] 20 | } 21 | } ] 22 | } 23 | 24 | -------------------------------------------------------------------------------- /source/chapter01/stringcodes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter01/stringcodes.py 4 | 5 | if __name__ == '__main__': 6 | # Translating from the outside world of bytes to Unicode characters. 7 | input_bytes = b'\xff\xfe4\x001\x003\x00 \x00i\x00s\x00 \x00i\x00n\x00.\x00' 8 | input_characters = input_bytes.decode('utf-16') 9 | print(repr(input_characters)) 10 | 11 | # Translating characters back into bytes before sending them. 12 | output_characters = 'We copy you down, Eagle.\n' 13 | output_bytes = output_characters.encode('utf-8') 14 | with open('eagle.txt', 'wb') as f: 15 | f.write(output_bytes) 16 | -------------------------------------------------------------------------------- /source/chapter02/README.md: -------------------------------------------------------------------------------- 1 | [Return to the Table of Contents](https://github.com/brandon-rhodes/fopnp#readme) 2 | 3 | # Chapter 2
UDP 4 | 5 | This is a directory of program listings from Chapter 2 of the book: 6 | 7 |
8 |
Foundations of Python Network Programming
9 |
10 | Third Edition, October 2014
11 | by Brandon Rhodes and John Goerzen 12 |
13 |
14 | 15 | You can learn more about the book by visiting the 16 | [root of this GitHub source code repository](https://github.com/brandon-rhodes/fopnp#readme). 17 | 18 | These scripts were written for Python 3, but can also run successfully 19 | under Python 2. Simply use [3to2](https://pypi.python.org/pypi/3to2) to 20 | convert them to the older syntax. 21 | 22 | One of the scripts is quite simple. It tries to send a large UDP 23 | datagram to learn the length of the largest packet that could actually 24 | cross the network between its own host and the host named on the command 25 | line. Running it in the [Playground](../../playground#readme) should 26 | report a maximum packet size of 1,500 bytes, the typical maximum length 27 | on an Ethernet network: 28 | 29 | ``` 30 | $ python3 big_sender.py www.example.com 31 | Alas, the datagram did not make it 32 | Actual MTU: 1500 33 | ``` 34 | 35 | The other scripts are each a client-server pair, where the server should 36 | be started in one terminal window and the client run in another. The 37 | `udp_local.py` server and client must be run on the same machine: 38 | 39 | ``` 40 | $ python3 udp_local.py server &>server.log & 41 | ``` 42 | 43 | ``` 44 | $ python3 udp_local.py client 45 | The OS assigned me the address ('0.0.0.0', 60442) 46 | The server ('127.0.0.1', 1060) replied 'Your data was 38 bytes long' 47 | ``` 48 | 49 | ``` 50 | $ cat server.log 51 | Listening at ('127.0.0.1', 1060) 52 | The client at ('127.0.0.1', 60442) says 'The time is 2014-10-22 14:52:25.936376' 53 | ``` 54 | 55 | The other two scripts can communicate between machines, and need 56 | hostname arguments on the command line. Read their source code, as well 57 | as the book chapter, to learn how the scripts demonstrate successively 58 | more powerful UDP abilities. 59 | -------------------------------------------------------------------------------- /source/chapter02/big_sender.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter02/big_sender.py 4 | # Send a big UDP datagram to learn the MTU of the network path. 5 | 6 | import IN, argparse, socket 7 | 8 | if not hasattr(IN, 'IP_MTU'): 9 | raise RuntimeError('cannot perform MTU discovery on this combination' 10 | ' of operating system and Python distribution') 11 | 12 | def send_big_datagram(host, port): 13 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 14 | sock.setsockopt(socket.IPPROTO_IP, IN.IP_MTU_DISCOVER, IN.IP_PMTUDISC_DO) 15 | sock.connect((host, port)) 16 | try: 17 | sock.send(b'#' * 999999) 18 | except socket.error: 19 | print('Alas, the datagram did not make it') 20 | max_mtu = sock.getsockopt(socket.IPPROTO_IP, IN.IP_MTU) 21 | print('Actual MTU: {}'.format(max_mtu)) 22 | else: 23 | print('The big datagram was sent!') 24 | 25 | if __name__ == '__main__': 26 | parser = argparse.ArgumentParser(description='Send UDP packet to get MTU') 27 | parser.add_argument('host', help='the host to which to target the packet') 28 | parser.add_argument('-p', metavar='PORT', type=int, default=1060, 29 | help='UDP port (default 1060)') 30 | args = parser.parse_args() 31 | send_big_datagram(args.host, args.p) 32 | -------------------------------------------------------------------------------- /source/chapter02/udp_broadcast.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter02/udp_broadcast.py 4 | # UDP client and server for broadcast messages on a local LAN 5 | 6 | import argparse, socket 7 | 8 | BUFSIZE = 65535 9 | 10 | def server(interface, port): 11 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 12 | sock.bind((interface, port)) 13 | print('Listening for datagrams at {}'.format(sock.getsockname())) 14 | while True: 15 | data, address = sock.recvfrom(BUFSIZE) 16 | text = data.decode('ascii') 17 | print('The client at {} says: {!r}'.format(address, text)) 18 | 19 | def client(network, port): 20 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 21 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 22 | text = 'Broadcast datagram!' 23 | sock.sendto(text.encode('ascii'), (network, port)) 24 | 25 | if __name__ == '__main__': 26 | choices = {'client': client, 'server': server} 27 | parser = argparse.ArgumentParser(description='Send, receive UDP broadcast') 28 | parser.add_argument('role', choices=choices, help='which role to take') 29 | parser.add_argument('host', help='interface the server listens at;' 30 | ' network the client sends to') 31 | parser.add_argument('-p', metavar='port', type=int, default=1060, 32 | help='UDP port (default 1060)') 33 | args = parser.parse_args() 34 | function = choices[args.role] 35 | function(args.host, args.p) 36 | -------------------------------------------------------------------------------- /source/chapter02/udp_local.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter02/udp_local.py 4 | # UDP client and server on localhost 5 | 6 | import argparse, socket 7 | from datetime import datetime 8 | 9 | MAX_BYTES = 65535 10 | 11 | def server(port): 12 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 13 | sock.bind(('127.0.0.1', port)) 14 | print('Listening at {}'.format(sock.getsockname())) 15 | while True: 16 | data, address = sock.recvfrom(MAX_BYTES) 17 | text = data.decode('ascii') 18 | print('The client at {} says {!r}'.format(address, text)) 19 | text = 'Your data was {} bytes long'.format(len(data)) 20 | data = text.encode('ascii') 21 | sock.sendto(data, address) 22 | 23 | def client(port): 24 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 25 | text = 'The time is {}'.format(datetime.now()) 26 | data = text.encode('ascii') 27 | sock.sendto(data, ('127.0.0.1', port)) 28 | print('The OS assigned me the address {}'.format(sock.getsockname())) 29 | data, address = sock.recvfrom(MAX_BYTES) # Danger! See Chapter 2 30 | text = data.decode('ascii') 31 | print('The server {} replied {!r}'.format(address, text)) 32 | 33 | if __name__ == '__main__': 34 | choices = {'client': client, 'server': server} 35 | parser = argparse.ArgumentParser(description='Send and receive UDP locally') 36 | parser.add_argument('role', choices=choices, help='which role to play') 37 | parser.add_argument('-p', metavar='PORT', type=int, default=1060, 38 | help='UDP port (default 1060)') 39 | args = parser.parse_args() 40 | function = choices[args.role] 41 | function(args.p) 42 | -------------------------------------------------------------------------------- /source/chapter02/udp_remote.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter02/udp_remote.py 4 | # UDP client and server for talking over the network 5 | 6 | import argparse, random, socket, sys 7 | 8 | MAX_BYTES = 65535 9 | 10 | def server(interface, port): 11 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 12 | sock.bind((interface, port)) 13 | print('Listening at', sock.getsockname()) 14 | while True: 15 | data, address = sock.recvfrom(MAX_BYTES) 16 | if random.random() < 0.5: 17 | print('Pretending to drop packet from {}'.format(address)) 18 | continue 19 | text = data.decode('ascii') 20 | print('The client at {} says {!r}'.format(address, text)) 21 | message = 'Your data was {} bytes long'.format(len(data)) 22 | sock.sendto(message.encode('ascii'), address) 23 | 24 | def client(hostname, port): 25 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 26 | hostname = sys.argv[2] 27 | sock.connect((hostname, port)) 28 | print('Client socket name is {}'.format(sock.getsockname())) 29 | 30 | delay = 0.1 # seconds 31 | text = 'This is another message' 32 | data = text.encode('ascii') 33 | while True: 34 | sock.send(data) 35 | print('Waiting up to {} seconds for a reply'.format(delay)) 36 | sock.settimeout(delay) 37 | try: 38 | data = sock.recv(MAX_BYTES) 39 | except socket.timeout as exc: 40 | delay *= 2 # wait even longer for the next request 41 | if delay > 2.0: 42 | raise RuntimeError('I think the server is down') from exc 43 | else: 44 | break # we are done, and can stop looping 45 | 46 | print('The server says {!r}'.format(data.decode('ascii'))) 47 | 48 | if __name__ == '__main__': 49 | choices = {'client': client, 'server': server} 50 | parser = argparse.ArgumentParser(description='Send and receive UDP,' 51 | ' pretending packets are often dropped') 52 | parser.add_argument('role', choices=choices, help='which role to take') 53 | parser.add_argument('host', help='interface the server listens at;' 54 | ' host the client sends to') 55 | parser.add_argument('-p', metavar='PORT', type=int, default=1060, 56 | help='UDP port (default 1060)') 57 | args = parser.parse_args() 58 | function = choices[args.role] 59 | function(args.host, args.p) 60 | -------------------------------------------------------------------------------- /source/chapter03/README.md: -------------------------------------------------------------------------------- 1 | [Return to the Table of Contents](https://github.com/brandon-rhodes/fopnp#readme) 2 | 3 | # Chapter 3
TCP 4 | 5 | This is a directory of program listings from Chapter 3 of the book: 6 | 7 |
8 |
Foundations of Python Network Programming
9 |
10 | Third Edition, October 2014
11 | by Brandon Rhodes and John Goerzen 12 |
13 |
14 | 15 | You can learn more about the book by visiting the 16 | [root of this GitHub source code repository](https://github.com/brandon-rhodes/fopnp#readme). 17 | 18 | These scripts were written for Python 3, but can also run successfully 19 | under Python 2. Simply use [3to2](https://pypi.python.org/pypi/3to2) to 20 | convert them to the older syntax. 21 | 22 | There are only two scripts featured in this chapter, both of which 23 | illustrate the TCP stream oriented protocol through a small client and 24 | server. Neither script needs its client and server to be run on 25 | different hosts to illustrate the features of TCP, though their command 26 | line arguments do permit it. The `tcp_sixteen.py` script simply sends 27 | and receives sixteen bytes of data in each direction: 28 | 29 | ``` 30 | $ python3 tcp_sixteen.py server '' &>server.log & 31 | ``` 32 | 33 | ``` 34 | $ python3 tcp_sixteen.py client localhost 35 | Client has been assigned socket name ('127.0.0.1', 34183) 36 | The server said b'Farewell, client' 37 | ``` 38 | 39 | ``` 40 | $ cat server.log 41 | Listening at ('0.0.0.0', 1060) 42 | Waiting to accept a new connection 43 | We have accepted a connection from ('127.0.0.1', 34183) 44 | Socket name: ('127.0.0.1', 1060) 45 | Socket peer: ('127.0.0.1', 34183) 46 | Incoming sixteen-octet message: b'Hi there, server' 47 | Reply sent, socket closed 48 | Waiting to accept a new connection 49 | ``` 50 | 51 | The `tcp_deadlock.py` script, by contrast, illustrates how incautious 52 | use of a stream protocol can fill the operating system network buffers 53 | and lead to deadlock. 54 | -------------------------------------------------------------------------------- /source/chapter03/tcp_deadlock.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter03/tcp_deadlock.py 4 | # TCP client and server that leave too much data waiting 5 | 6 | import argparse, socket, sys 7 | 8 | def server(host, port, bytecount): 9 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 10 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 11 | sock.bind((host, port)) 12 | sock.listen(1) 13 | print('Listening at', sock.getsockname()) 14 | while True: 15 | sc, sockname = sock.accept() 16 | print('Processing up to 1024 bytes at a time from', sockname) 17 | n = 0 18 | while True: 19 | data = sc.recv(1024) 20 | if not data: 21 | break 22 | output = data.decode('ascii').upper().encode('ascii') 23 | sc.sendall(output) # send it back uppercase 24 | n += len(data) 25 | print('\r %d bytes processed so far' % (n,), end=' ') 26 | sys.stdout.flush() 27 | print() 28 | sc.close() 29 | print(' Socket closed') 30 | 31 | def client(host, port, bytecount): 32 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 33 | bytecount = (bytecount + 15) // 16 * 16 # round up to a multiple of 16 34 | message = b'capitalize this!' # 16-byte message to repeat over and over 35 | 36 | print('Sending', bytecount, 'bytes of data, in chunks of 16 bytes') 37 | sock.connect((host, port)) 38 | 39 | sent = 0 40 | while sent < bytecount: 41 | sock.sendall(message) 42 | sent += len(message) 43 | print('\r %d bytes sent' % (sent,), end=' ') 44 | sys.stdout.flush() 45 | 46 | print() 47 | sock.shutdown(socket.SHUT_WR) 48 | 49 | print('Receiving all the data the server sends back') 50 | 51 | received = 0 52 | while True: 53 | data = sock.recv(42) 54 | if not received: 55 | print(' The first data received says', repr(data)) 56 | if not data: 57 | break 58 | received += len(data) 59 | print('\r %d bytes received' % (received,), end=' ') 60 | 61 | print() 62 | sock.close() 63 | 64 | if __name__ == '__main__': 65 | choices = {'client': client, 'server': server} 66 | parser = argparse.ArgumentParser(description='Get deadlocked over TCP') 67 | parser.add_argument('role', choices=choices, help='which role to play') 68 | parser.add_argument('host', help='interface the server listens at;' 69 | ' host the client sends to') 70 | parser.add_argument('bytecount', type=int, nargs='?', default=16, 71 | help='number of bytes for client to send (default 16)') 72 | parser.add_argument('-p', metavar='PORT', type=int, default=1060, 73 | help='TCP port (default 1060)') 74 | args = parser.parse_args() 75 | function = choices[args.role] 76 | function(args.host, args.p, args.bytecount) 77 | -------------------------------------------------------------------------------- /source/chapter03/tcp_sixteen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter03/tcp_sixteen.py 4 | # Simple TCP client and server that send and receive 16 octets 5 | 6 | import argparse, socket 7 | 8 | def recvall(sock, length): 9 | data = b'' 10 | while len(data) < length: 11 | more = sock.recv(length - len(data)) 12 | if not more: 13 | raise EOFError('was expecting %d bytes but only received' 14 | ' %d bytes before the socket closed' 15 | % (length, len(data))) 16 | data += more 17 | return data 18 | 19 | def server(interface, port): 20 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 21 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 22 | sock.bind((interface, port)) 23 | sock.listen(1) 24 | print('Listening at', sock.getsockname()) 25 | while True: 26 | print('Waiting to accept a new connection') 27 | sc, sockname = sock.accept() 28 | print('We have accepted a connection from', sockname) 29 | print(' Socket name:', sc.getsockname()) 30 | print(' Socket peer:', sc.getpeername()) 31 | message = recvall(sc, 16) 32 | print(' Incoming sixteen-octet message:', repr(message)) 33 | sc.sendall(b'Farewell, client') 34 | sc.close() 35 | print(' Reply sent, socket closed') 36 | 37 | def client(host, port): 38 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 39 | sock.connect((host, port)) 40 | print('Client has been assigned socket name', sock.getsockname()) 41 | sock.sendall(b'Hi there, server') 42 | reply = recvall(sock, 16) 43 | print('The server said', repr(reply)) 44 | sock.close() 45 | 46 | if __name__ == '__main__': 47 | choices = {'client': client, 'server': server} 48 | parser = argparse.ArgumentParser(description='Send and receive over TCP') 49 | parser.add_argument('role', choices=choices, help='which role to play') 50 | parser.add_argument('host', help='interface the server listens at;' 51 | ' host the client sends to') 52 | parser.add_argument('-p', metavar='PORT', type=int, default=1060, 53 | help='TCP port (default 1060)') 54 | args = parser.parse_args() 55 | function = choices[args.role] 56 | function(args.host, args.p) 57 | -------------------------------------------------------------------------------- /source/chapter04/README.md: -------------------------------------------------------------------------------- 1 | [Return to the Table of Contents](https://github.com/brandon-rhodes/fopnp#readme) 2 | 3 | # Chapter 4
Socket Names and DNS 4 | 5 | This is a directory of program listings from Chapter 4 of the book: 6 | 7 |
8 |
Foundations of Python Network Programming
9 |
10 | Third Edition, October 2014
11 | by Brandon Rhodes and John Goerzen 12 |
13 |
14 | 15 | You can learn more about the book by visiting the 16 | [root of this GitHub source code repository](https://github.com/brandon-rhodes/fopnp#readme). 17 | 18 | These scripts were written for Python 3, but most of them will run under 19 | Python 2. Use [3to2](https://pypi.python.org/pypi/3to2) to convert them 20 | to the older syntax. The `www_ping.py` script will then need a small 21 | tweak: change the string literal `u'www'` to simply `'www'` in its call 22 | to `getaddrinfo()` or you will get a socket error. 23 | 24 | The chapter gives a tour of DNS and how it provides worldwide name 25 | resolution. The three scripts involved are conceptually quite simple. 26 | The first asks the operating system to do its usual name resolution, and 27 | presents the normal pattern by which Python programs should resolve a 28 | name before opening a socket. 29 | 30 | ``` 31 | $ python3 www_ping.py www.google.com 32 | Success: host www.google.com is listening on port 80 33 | ``` 34 | 35 | The second two dig into the DNS protocol itself using the third-party 36 | `dnspython3` package: 37 | 38 | ``` 39 | $ python3 dns_basic.py www.google.com 40 | 41 | 42 | ``` 43 | 44 | ``` 45 | $ python3 dns_mx.py google.com 46 | This domain has 5 MX records 47 | Priority 10 48 | aspmx.l.google.com has A address 64.233.171.27 49 | Priority 20 50 | alt1.aspmx.l.google.com has A address 64.233.186.27 51 | Priority 30 52 | alt2.aspmx.l.google.com has A address 74.125.24.27 53 | Priority 40 54 | alt3.aspmx.l.google.com has A address 74.125.206.27 55 | Priority 50 56 | alt4.aspmx.l.google.com has A address 173.194.65.27 57 | ``` 58 | 59 | The chapter outlines how to properly use the logic inside of `dns_mx.py` 60 | to power your own SMTP email delivery system so that it obeys the 61 | relevant standards for sending messages to big sites like Google. 62 | -------------------------------------------------------------------------------- /source/chapter04/dns_basic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter04/dns_basic.py 4 | # Basic DNS query 5 | 6 | import argparse, dns.resolver 7 | 8 | def lookup(name): 9 | for qtype in 'A', 'AAAA', 'CNAME', 'MX', 'NS': 10 | answer = dns.resolver.query(name, qtype, raise_on_no_answer=False) 11 | if answer.rrset is not None: 12 | print(answer.rrset) 13 | 14 | if __name__ == '__main__': 15 | parser = argparse.ArgumentParser(description='Resolve a name using DNS') 16 | parser.add_argument('name', help='name that you want to look up in DNS') 17 | lookup(parser.parse_args().name) 18 | -------------------------------------------------------------------------------- /source/chapter04/dns_mx.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter04/dns_mx.py 4 | # Looking up a mail domain - the part of an email address after the `@` 5 | 6 | import argparse, dns.resolver 7 | 8 | def resolve_hostname(hostname, indent=''): 9 | "Print an A or AAAA record for `hostname`; follow CNAMEs if necessary." 10 | indent = indent + ' ' 11 | answer = dns.resolver.query(hostname, 'A') 12 | if answer.rrset is not None: 13 | for record in answer: 14 | print(indent, hostname, 'has A address', record.address) 15 | return 16 | answer = dns.resolver.query(hostname, 'AAAA') 17 | if answer.rrset is not None: 18 | for record in answer: 19 | print(indent, hostname, 'has AAAA address', record.address) 20 | return 21 | answer = dns.resolver.query(hostname, 'CNAME') 22 | if answer.rrset is not None: 23 | record = answer[0] 24 | cname = record.address 25 | print(indent, hostname, 'is a CNAME alias for', cname) #? 26 | resolve_hostname(cname, indent) 27 | return 28 | print(indent, 'ERROR: no A, AAAA, or CNAME records for', hostname) 29 | 30 | def resolve_email_domain(domain): 31 | "For an email address `name@domain` find its mail server IP addresses." 32 | try: 33 | answer = dns.resolver.query(domain, 'MX', raise_on_no_answer=False) 34 | except dns.resolver.NXDOMAIN: 35 | print('Error: No such domain', domain) 36 | return 37 | if answer.rrset is not None: 38 | records = sorted(answer, key=lambda record: record.preference) 39 | print('This domain has', len(records), 'MX records') 40 | for record in records: 41 | name = record.exchange.to_text(omit_final_dot=True) 42 | print('Priority', record.preference) 43 | resolve_hostname(name) 44 | else: 45 | print('This domain has no explicit MX records') 46 | print('Attempting to resolve it as an A, AAAA, or CNAME') 47 | resolve_hostname(domain) 48 | 49 | if __name__ == '__main__': 50 | parser = argparse.ArgumentParser(description='Find mailserver IP address') 51 | parser.add_argument('domain', help='domain that you want to send mail to') 52 | resolve_email_domain(parser.parse_args().domain) 53 | -------------------------------------------------------------------------------- /source/chapter04/www_ping.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter04/www_ping.py 4 | # Find the WWW service of an arbitrary host using getaddrinfo(). 5 | 6 | import argparse, socket, sys 7 | 8 | def connect_to(hostname_or_ip): 9 | try: 10 | infolist = socket.getaddrinfo( 11 | hostname_or_ip, 'www', 0, socket.SOCK_STREAM, 0, 12 | socket.AI_ADDRCONFIG | socket.AI_V4MAPPED | socket.AI_CANONNAME, 13 | ) 14 | except socket.gaierror as e: 15 | print('Name service failure:', e.args[1]) 16 | sys.exit(1) 17 | 18 | info = infolist[0] # per standard recommendation, try the first one 19 | socket_args = info[0:3] 20 | address = info[4] 21 | s = socket.socket(*socket_args) 22 | try: 23 | s.connect(address) 24 | except socket.error as e: 25 | print('Network failure:', e.args[1]) 26 | else: 27 | print('Success: host', info[3], 'is listening on port 80') 28 | 29 | if __name__ == '__main__': 30 | parser = argparse.ArgumentParser(description='Try connecting to port 80') 31 | parser.add_argument('hostname', help='hostname that you want to contact') 32 | connect_to(parser.parse_args().hostname) 33 | -------------------------------------------------------------------------------- /source/chapter05/README.md: -------------------------------------------------------------------------------- 1 | [Return to the Table of Contents](https://github.com/brandon-rhodes/fopnp#readme) 2 | 3 | # Chapter 5
Network Data and Network Errors 4 | 5 | This is a directory of program listings from Chapter 5 of the book: 6 | 7 |
8 |
Foundations of Python Network Programming
9 |
10 | Third Edition, October 2014
11 | by Brandon Rhodes and John Goerzen 12 |
13 |
14 | 15 | You can learn more about the book by visiting the 16 | [root of this GitHub source code repository](https://github.com/brandon-rhodes/fopnp#readme). 17 | 18 | These scripts were written for Python 3, but can also run successfully 19 | under Python 2. Simply use [3to2](https://pypi.python.org/pypi/3to2) to 20 | convert them to the older syntax. 21 | 22 | This chapter tackles the issue of encoding and framing network data, 23 | discussing many small examples that are collected here in the source 24 | code repository as the `examples.rst` file. In addition, there are two 25 | scripts that then bring together the bigger ideas in the chapter. The 26 | first is a small client and server that perform no framing, but simply 27 | close the socket to let their peer know when they are done: 28 | 29 | ``` 30 | $ python3 streamer.py '' &>server.log & 31 | ``` 32 | 33 | ``` 34 | $ python3 streamer.py -c localhost 35 | ``` 36 | 37 | ``` 38 | $ cat server.log 39 | Run this script in another window with "-c" to connect 40 | Listening at ('0.0.0.0', 1060) 41 | Accepted connection from ('127.0.0.1', 40613) 42 | Received 96 bytes 43 | Received zero bytes - end of file 44 | Message: 45 | 46 | Beautiful is better than ugly. 47 | Explicit is better than implicit. 48 | Simple is better than complex. 49 | 50 | ``` 51 | 52 | The second client prepends explicit binary framing into each message it 53 | sends, so that the server can extract them each from the socket stream 54 | without ambiguity. 55 | 56 | ``` 57 | $ python3 blocks.py '' &>server.log & 58 | ``` 59 | 60 | ``` 61 | $ python3 blocks.py -c localhost 62 | ``` 63 | 64 | ``` 65 | $ cat server.log 66 | Run this script in another window with "-c" to connect 67 | Listening at ('0.0.0.0', 1060) 68 | Accepted connection from ('127.0.0.1', 40615) 69 | Block says: b'Beautiful is better than ugly.' 70 | Block says: b'Explicit is better than implicit.' 71 | Block says: b'Simple is better than complex.' 72 | ``` 73 | 74 | In retrospect, these clients are a little bit quiet for my taste. They 75 | should probably have at least printed something to their standard out, 76 | so that it is clear that they are trying to accomplish something. As it 77 | stands, the user has to look at the terminal window where the server is 78 | running to be convinced that the client really sent anything over the 79 | network. 80 | 81 | See the book chapter for the lessons that can be drawn from each of 82 | these two designs, and about the other possible approaches — beyond this 83 | simple pair of mechanisms — that network protocols can adopt. 84 | -------------------------------------------------------------------------------- /source/chapter05/blocks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter05/blocks.py 4 | # Sending data over a stream but delimited as length-prefixed blocks. 5 | 6 | import socket, struct 7 | from argparse import ArgumentParser 8 | 9 | header_struct = struct.Struct('!I') # messages up to 2**32 - 1 in length 10 | 11 | def recvall(sock, length): 12 | blocks = [] 13 | while length: 14 | block = sock.recv(length) 15 | if not block: 16 | raise EOFError('socket closed with %d bytes left' 17 | ' in this block'.format(length)) 18 | length -= len(block) 19 | blocks.append(block) 20 | return b''.join(blocks) 21 | 22 | def get_block(sock): 23 | data = recvall(sock, header_struct.size) 24 | (block_length,) = header_struct.unpack(data) 25 | return recvall(sock, block_length) 26 | 27 | def put_block(sock, message): 28 | block_length = len(message) 29 | sock.send(header_struct.pack(block_length)) 30 | sock.send(message) 31 | 32 | def server(address): 33 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 34 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 35 | sock.bind(address) 36 | sock.listen(1) 37 | print('Run this script in another window with "-c" to connect') 38 | print('Listening at', sock.getsockname()) 39 | sc, sockname = sock.accept() 40 | print('Accepted connection from', sockname) 41 | sc.shutdown(socket.SHUT_WR) 42 | while True: 43 | block = get_block(sc) 44 | if not block: 45 | break 46 | print('Block says:', repr(block)) 47 | sc.close() 48 | sock.close() 49 | 50 | def client(address): 51 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 52 | sock.connect(address) 53 | sock.shutdown(socket.SHUT_RD) 54 | put_block(sock, b'Beautiful is better than ugly.') 55 | put_block(sock, b'Explicit is better than implicit.') 56 | put_block(sock, b'Simple is better than complex.') 57 | put_block(sock, b'') 58 | sock.close() 59 | 60 | if __name__ == '__main__': 61 | parser = ArgumentParser(description='Transmit & receive blocks over TCP') 62 | parser.add_argument('hostname', nargs='?', default='127.0.0.1', 63 | help='IP address or hostname (default: %(default)s)') 64 | parser.add_argument('-c', action='store_true', help='run as the client') 65 | parser.add_argument('-p', type=int, metavar='port', default=1060, 66 | help='TCP port number (default: %(default)s)') 67 | args = parser.parse_args() 68 | function = client if args.c else server 69 | function((args.hostname, args.p)) 70 | -------------------------------------------------------------------------------- /source/chapter05/examples.rst: -------------------------------------------------------------------------------- 1 | 2 | >>> b'\x67\x68\x69\xe7\xe8\xe9'.decode('latin1') 3 | 'ghiçèé' 4 | >>> b'\x67\x68\x69\xe7\xe8\xe9'.decode('latin2') 5 | 'ghiçčé' 6 | >>> b'\x67\x68\x69\xe7\xe8\xe9'.decode('greek') 7 | 'ghiηθι' 8 | >>> b'\x67\x68\x69\xe7\xe8\xe9'.decode('hebrew') 9 | 'ghiחטי' 10 | 11 | >>> b'\x67\x68\x69\xe7\xe8\xe9'.decode('EBCDIC-CP-BE') 12 | 'ÅÇÑXYZ' 13 | 14 | >>> len('Namárië!') 15 | 8 16 | >>> 'Namárië!'.encode('UTF-16') 17 | b'\xff\xfeN\x00a\x00m\x00\xe1\x00r\x00i\x00\xeb\x00!\x00' 18 | >>> len(_) 19 | 18 20 | >>> 'Namárië!'.encode('UTF-32') 21 | b'\xff\xfe\x00\x00N\x00\x00\x00a\x00\x00\x00m\x00\x00\x00\xe1\x00\x00\x00r\x00\x00\x00i\x00\x00\x00\xeb\x00\x00\x00!\x00\x00\x00' 22 | >>> len(_) 23 | 36 24 | >>> 'Namárië!'.encode('UTF-8') 25 | b'Nam\xc3\xa1ri\xc3\xab!' 26 | >>> len(_) 27 | 10 28 | 29 | >>> b'\x80'.decode('ascii') 30 | Traceback (most recent call last): 31 | ... 32 | UnicodeDecodeError: 'ascii' codec can't decode byte 0x80 in position 0: ordinal not in range(128) 33 | >>> 'ghiηθι'.encode('latin-1') 34 | Traceback (most recent call last): 35 | ... 36 | UnicodeEncodeError: 'latin-1' codec can't encode characters in position 3-5: ordinal not in range(256) 37 | 38 | >>> b'ab\x80def'.decode('ascii', 'replace') 39 | 'ab�def' 40 | >>> b'ab\x80def'.decode('ascii', 'ignore') 41 | 'abdef' 42 | >>> 'ghiηθι'.encode('latin-1', 'replace') 43 | b'ghi???' 44 | >>> 'ghiηθι'.encode('latin-1', 'ignore') 45 | b'ghi' 46 | 47 | >>> import struct 48 | >>> struct.pack('>> struct.pack('>i', 4253) 51 | b'\x00\x00\x10\x9d' 52 | >>> struct.unpack('>i', b'\x00\x00\x10\x9d') 53 | (4253,) 54 | 55 | >>> import pickle 56 | >>> pickle.dumps([5, 6, 7]) 57 | b'\x80\x03]q\x00(K\x05K\x06K\x07e.' 58 | 59 | >>> pickle.loads(b'\x80\x03]q\x00(K\x05K\x06K\x07e.blahblahblah') 60 | [5, 6, 7] 61 | 62 | >>> from io import BytesIO 63 | >>> f = BytesIO(b'\x80\x03]q\x00(K\x05K\x06K\x07e.blahblahblah') 64 | >>> pickle.load(f) 65 | [5, 6, 7] 66 | >>> f.tell() 67 | 14 68 | >>> f.read() 69 | b'blahblahblah' 70 | 71 | -------------------------------------------------------------------------------- /source/chapter05/streamer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter05/streamer.py 4 | # Client that sends data then closes the socket, not expecting a reply. 5 | 6 | import socket 7 | from argparse import ArgumentParser 8 | 9 | def server(address): 10 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 11 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 12 | sock.bind(address) 13 | sock.listen(1) 14 | print('Run this script in another window with "-c" to connect') 15 | print('Listening at', sock.getsockname()) 16 | sc, sockname = sock.accept() 17 | print('Accepted connection from', sockname) 18 | sc.shutdown(socket.SHUT_WR) 19 | message = b'' 20 | while True: 21 | more = sc.recv(8192) # arbitrary value of 8k 22 | if not more: # socket has closed when recv() returns '' 23 | print('Received zero bytes - end of file') 24 | break 25 | print('Received {} bytes'.format(len(more))) 26 | message += more 27 | print('Message:\n') 28 | print(message.decode('ascii')) 29 | sc.close() 30 | sock.close() 31 | 32 | def client(address): 33 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 34 | sock.connect(address) 35 | sock.shutdown(socket.SHUT_RD) 36 | sock.sendall(b'Beautiful is better than ugly.\n') 37 | sock.sendall(b'Explicit is better than implicit.\n') 38 | sock.sendall(b'Simple is better than complex.\n') 39 | sock.close() 40 | 41 | if __name__ == '__main__': 42 | parser = ArgumentParser(description='Transmit & receive a data stream') 43 | parser.add_argument('hostname', nargs='?', default='127.0.0.1', 44 | help='IP address or hostname (default: %(default)s)') 45 | parser.add_argument('-c', action='store_true', help='run as the client') 46 | parser.add_argument('-p', type=int, metavar='port', default=1060, 47 | help='TCP port number (default: %(default)s)') 48 | args = parser.parse_args() 49 | function = client if args.c else server 50 | function((args.hostname, args.p)) 51 | -------------------------------------------------------------------------------- /source/chapter06/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFUzCCA7ugAwIBAgIJAOKCwFsBpeFGMA0GCSqGSIb3DQEBBQUAMIHFMQswCQYD 3 | VQQGEwJ1czERMA8GA1UECBMITmV3IFlvcmsxETAPBgNVBAcTCE5ldyBZb3JrMSkw 4 | JwYDVQQKEyBFeGFtcGxlIENBIGZyb20gQXByZXNzIE1lZGlhIExMQzE5MDcGA1UE 5 | CxMwRm91bmRhdGlvbnMgb2YgUHl0aG9uIE5ldHdvcmsgUHJvZ3JhbW1pbmcgM3Jk 6 | IEVkMQswCQYDVQQDEwJjYTEdMBsGCSqGSIb3DQEJARYOY2FAZXhhbXBsZS5jb20w 7 | IBcNMTQwMzA4MTY1NjU2WhgPMjExNDAyMTIxNjU2NTZaMIHFMQswCQYDVQQGEwJ1 8 | czERMA8GA1UECBMITmV3IFlvcmsxETAPBgNVBAcTCE5ldyBZb3JrMSkwJwYDVQQK 9 | EyBFeGFtcGxlIENBIGZyb20gQXByZXNzIE1lZGlhIExMQzE5MDcGA1UECxMwRm91 10 | bmRhdGlvbnMgb2YgUHl0aG9uIE5ldHdvcmsgUHJvZ3JhbW1pbmcgM3JkIEVkMQsw 11 | CQYDVQQDEwJjYTEdMBsGCSqGSIb3DQEJARYOY2FAZXhhbXBsZS5jb20wggGiMA0G 12 | CSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDAVZzfmYvPo8nYXJt4zz2qkCRZhJIv 13 | lRNkNmNMStg9aW2FbeKUv3se+SX4dWsOw1dtxJJfWBvzk93R0qPvZOkaa41v3XIg 14 | riWNYt2ctid7l4EDYIVO42NfJLkfo5I/RCHZW9f7rLMBPyrU/27CEdfpN4PVQZf5 15 | SiG61UiPhxfG6YUC+QPcmXL468jsWOQnH+PutThkw9CGhFp/+d/M/OejWA5yQnmJ 16 | lp6jWVVLoqlwr5eRuEC8v2bunVviHW0xYhlvODYbMEqf1zeiXcb9tlkNHBRNENya 17 | N3jCJ3RxAbOGVpRIvP2EVf4vSzmdqrg9LqkzM4LE6e2aNqpmn84bsE1NGN1hbJAe 18 | JwZhxzThIH9674oE1Xxg+2vmgTcq12p4GzdoCQJh45fnOXA/2AtJVD/MQtZ1IgzL 19 | yPH85rZZnNOAJWxqkkdvToDsocT99ykOz45+OBvqFbhbWWD75zWuXh70U/gnu/L0 20 | XZcxWMNacMtzDUpFj9k+pUm1Ju6HYZ17JYcCAwEAAaNCMEAwHQYDVR0OBBYEFDyH 21 | jI3fNE9b9psr5lXJK9iZiYwxMB8GA1UdIwQYMBaAFDyHjI3fNE9b9psr5lXJK9iZ 22 | iYwxMA0GCSqGSIb3DQEBBQUAA4IBgQBLHxtbQzUySVdrP7JEr62CIXuO1DJXQhtr 23 | xyJOX3GeZo3jqtmd9o0W1LZKi81UbkiK2PtpvdOg/rumFsCsx0oqrurIEM2Pg8U5 24 | 92S10C1zfHQMzVlpv5DDQmYSN9FOcztz6oCXLZj9duHyUY35phIHtVCk2QiBB1vi 25 | 9DPEg0eDhT+dqvxRLj47lAPRUEtRyjmdIN0C7RFNvBb7VDJkPGXz+ZIZNuWd+xXe 26 | gSGlDsg/SOssEjPje75A4caZI9w25avhz46hQLpKxO1iWcGdl5QqKmrJ7Uta+vjq 27 | kodRYlINrWrI8HvwwZVd42GirWFEdLufvri3hgVu3DOo/ZDv6IdpoeC3qli61qlE 28 | PLiMV1kNM5gP+QBF7UQENE9l8378I3ne+u+eQACDHfOqYcFoa0lfMU5bbeg34Nbb 29 | eIE+ozE6ANJ/JPcpioivBuYaIrFQMLvD0QwPrzR5z4ex9zxDvGPV0anuoPtIhTDW 30 | OiYIVRa8e8GJKS764FKqn4w/FuyVlTo= 31 | -----END CERTIFICATE----- 32 | -------------------------------------------------------------------------------- /source/chapter06/features.py: -------------------------------------------------------------------------------- 1 | """Introspect Python's SSL library and list its features and options. 2 | 3 | This script was not included in the text of the book itself, but was a 4 | useful enough tool to me while I was writing the book that I thought I 5 | should leave it sitting around in the source code repository. 6 | 7 | - Brandon Rhodes, for Foundations of Python Network Programming, 8 | Third Edition 9 | 10 | """ 11 | try: 12 | import ssl 13 | except ImportError: 14 | ssl = None 15 | 16 | def main(): 17 | if ssl is None: 18 | print('This Python is not compiled with SSL support') 19 | return 20 | names = dir(ssl) 21 | print() 22 | display(names, ' protocol ', lambda s: s.startswith('PROTOCOL_')) 23 | display(names, ' verify_mode ', lambda s: s.startswith('CERT_')) 24 | display(names, ' verify_flags ', lambda s: s.startswith('VERIFY_')) 25 | display(names, ' options ', lambda s: s.startswith('OP_')) 26 | display(names, ' feature availability ', lambda s: s.startswith('HAS_')) 27 | 28 | def display(names, title, test): 29 | items = [(fix(getattr(ssl, name)), name) for name in names if test(name)] 30 | print(title.center(72, '-')) 31 | for value, name in sorted(items): 32 | print('{:27} {:10} {:>32}'.format(name, value, bin(value)[2:])) 33 | print() 34 | 35 | def fix(value): 36 | """Turn negative 32-bit numbers into positive numbers.""" 37 | return (value + 2 ** 32) if (value < 0) else value 38 | 39 | if __name__ == '__main__': 40 | main() 41 | -------------------------------------------------------------------------------- /source/chapter06/safe_tls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter06/safe_tls.py 4 | # Simple TLS client and server using safe configuration defaults 5 | 6 | import argparse, socket, ssl 7 | 8 | def client(host, port, cafile=None): 9 | purpose = ssl.Purpose.SERVER_AUTH 10 | context = ssl.create_default_context(purpose, cafile=cafile) 11 | 12 | raw_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 13 | raw_sock.connect((host, port)) 14 | print('Connected to host {!r} and port {}'.format(host, port)) 15 | ssl_sock = context.wrap_socket(raw_sock, server_hostname=host) 16 | 17 | while True: 18 | data = ssl_sock.recv(1024) 19 | if not data: 20 | break 21 | print(repr(data)) 22 | 23 | def server(host, port, certfile, cafile=None): 24 | purpose = ssl.Purpose.CLIENT_AUTH 25 | context = ssl.create_default_context(purpose, cafile=cafile) 26 | context.load_cert_chain(certfile) 27 | 28 | listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 29 | listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 30 | listener.bind((host, port)) 31 | listener.listen(1) 32 | print('Listening at interface {!r} and port {}'.format(host, port)) 33 | raw_sock, address = listener.accept() 34 | print('Connection from host {!r} and port {}'.format(*address)) 35 | ssl_sock = context.wrap_socket(raw_sock, server_side=True) 36 | 37 | ssl_sock.sendall('Simple is better than complex.'.encode('ascii')) 38 | ssl_sock.close() 39 | 40 | if __name__ == '__main__': 41 | parser = argparse.ArgumentParser(description='Safe TLS client and server') 42 | parser.add_argument('host', help='hostname or IP address') 43 | parser.add_argument('port', type=int, help='TCP port number') 44 | parser.add_argument('-a', metavar='cafile', default=None, 45 | help='authority: path to CA certificate PEM file') 46 | parser.add_argument('-s', metavar='certfile', default=None, 47 | help='run as server: path to server PEM file') 48 | args = parser.parse_args() 49 | if args.s: 50 | server(args.host, args.port, args.s, args.a) 51 | else: 52 | client(args.host, args.port, args.a) 53 | -------------------------------------------------------------------------------- /source/chapter07/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter07/client.py 4 | # Simple Zen-of-Python client that asks three questions then disconnects. 5 | 6 | import argparse, random, socket, zen_utils 7 | 8 | def client(address, cause_error=False): 9 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 10 | sock.connect(address) 11 | aphorisms = list(zen_utils.aphorisms) 12 | if cause_error: 13 | sock.sendall(aphorisms[0][:-1]) 14 | return 15 | for aphorism in random.sample(aphorisms, 3): 16 | sock.sendall(aphorism) 17 | print(aphorism, zen_utils.recv_until(sock, b'.')) 18 | sock.close() 19 | 20 | if __name__ == '__main__': 21 | parser = argparse.ArgumentParser(description='Example client') 22 | parser.add_argument('host', help='IP or hostname') 23 | parser.add_argument('-e', action='store_true', help='cause an error') 24 | parser.add_argument('-p', metavar='port', type=int, default=1060, 25 | help='TCP port (default 1060)') 26 | args = parser.parse_args() 27 | address = (args.host, args.p) 28 | client(address, args.e) 29 | -------------------------------------------------------------------------------- /source/chapter07/in_zen1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter07/in_zen1.py 4 | # Single-shot server for the use of inetd(8). 5 | 6 | import socket, sys, zen_utils 7 | 8 | if __name__ == '__main__': 9 | sock = socket.fromfd(0, socket.AF_INET, socket.SOCK_STREAM) 10 | sys.stdin = open('/dev/null', 'r') 11 | sys.stdout = sys.stderr = open('/tmp/zen.log', 'a', buffering=1) 12 | address = sock.getpeername() 13 | print('Accepted connection from {}'.format(address)) 14 | zen_utils.handle_conversation(sock, address) 15 | -------------------------------------------------------------------------------- /source/chapter07/in_zen2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter07/in_zen2.py 4 | # Multi-shot server for the use of inetd(8). 5 | 6 | import socket, sys, zen_utils 7 | 8 | if __name__ == '__main__': 9 | listener = socket.fromfd(0, socket.AF_INET, socket.SOCK_STREAM) 10 | sys.stdin = open('/dev/null', 'r') 11 | sys.stdout = sys.stderr = open('/tmp/zen.log', 'a', buffering=1) 12 | listener.settimeout(8.0) 13 | try: 14 | zen_utils.accept_connections_forever(listener) 15 | except socket.timeout: 16 | print('Waited 8 seconds with no further connections; shutting down') 17 | -------------------------------------------------------------------------------- /source/chapter07/inetd.conf: -------------------------------------------------------------------------------- 1 | 1060 stream tcp nowait brandon /usr/bin/python3 /usr/bin/python3 /in_zen1.py 2 | 1061 stream tcp wait brandon /usr/bin/python3 /usr/bin/python3 /in_zen2.py 3 | -------------------------------------------------------------------------------- /source/chapter07/srv_async.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter07/srv_async.py 4 | # Asynchronous I/O driven directly by the poll() system call. 5 | 6 | import select, zen_utils 7 | 8 | def all_events_forever(poll_object): 9 | while True: 10 | for fd, event in poll_object.poll(): 11 | yield fd, event 12 | 13 | def serve(listener): 14 | sockets = {listener.fileno(): listener} 15 | addresses = {} 16 | bytes_received = {} 17 | bytes_to_send = {} 18 | 19 | poll_object = select.poll() 20 | poll_object.register(listener, select.POLLIN) 21 | 22 | for fd, event in all_events_forever(poll_object): 23 | sock = sockets[fd] 24 | 25 | # Socket closed: remove it from our data structures. 26 | 27 | if event & (select.POLLHUP | select.POLLERR | select.POLLNVAL): 28 | address = addresses.pop(sock) 29 | rb = bytes_received.pop(sock, b'') 30 | sb = bytes_to_send.pop(sock, b'') 31 | if rb: 32 | print('Client {} sent {} but then closed'.format(address, rb)) 33 | elif sb: 34 | print('Client {} closed before we sent {}'.format(address, sb)) 35 | else: 36 | print('Client {} closed socket normally'.format(address)) 37 | poll_object.unregister(fd) 38 | del sockets[fd] 39 | 40 | # New socket: add it to our data structures. 41 | 42 | elif sock is listener: 43 | sock, address = sock.accept() 44 | print('Accepted connection from {}'.format(address)) 45 | sock.setblocking(False) # force socket.timeout if we blunder 46 | sockets[sock.fileno()] = sock 47 | addresses[sock] = address 48 | poll_object.register(sock, select.POLLIN) 49 | 50 | # Incoming data: keep receiving until we see the suffix. 51 | 52 | elif event & select.POLLIN: 53 | more_data = sock.recv(4096) 54 | if not more_data: # end-of-file 55 | sock.close() # next poll() will POLLNVAL, and thus clean up 56 | continue 57 | data = bytes_received.pop(sock, b'') + more_data 58 | if data.endswith(b'?'): 59 | bytes_to_send[sock] = zen_utils.get_answer(data) 60 | poll_object.modify(sock, select.POLLOUT) 61 | else: 62 | bytes_received[sock] = data 63 | 64 | # Socket ready to send: keep sending until all bytes are delivered. 65 | 66 | elif event & select.POLLOUT: 67 | data = bytes_to_send.pop(sock) 68 | n = sock.send(data) 69 | if n < len(data): 70 | bytes_to_send[sock] = data[n:] 71 | else: 72 | poll_object.modify(sock, select.POLLIN) 73 | 74 | if __name__ == '__main__': 75 | address = zen_utils.parse_command_line('low-level async server') 76 | listener = zen_utils.create_srv_socket(address) 77 | serve(listener) 78 | -------------------------------------------------------------------------------- /source/chapter07/srv_asyncio1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter07/srv_asyncio1.py 4 | # Asynchronous I/O inside "asyncio" callback methods. 5 | 6 | import asyncio, zen_utils 7 | 8 | class ZenServer(asyncio.Protocol): 9 | 10 | def connection_made(self, transport): 11 | self.transport = transport 12 | self.address = transport.get_extra_info('peername') 13 | self.data = b'' 14 | print('Accepted connection from {}'.format(self.address)) 15 | 16 | def data_received(self, data): 17 | self.data += data 18 | if self.data.endswith(b'?'): 19 | answer = zen_utils.get_answer(self.data) 20 | self.transport.write(answer) 21 | self.data = b'' 22 | 23 | def connection_lost(self, exc): 24 | if exc: 25 | print('Client {} error: {}'.format(self.address, exc)) 26 | elif self.data: 27 | print('Client {} sent {} but then closed' 28 | .format(self.address, self.data)) 29 | else: 30 | print('Client {} closed socket'.format(self.address)) 31 | 32 | if __name__ == '__main__': 33 | address = zen_utils.parse_command_line('asyncio server using callbacks') 34 | loop = asyncio.get_event_loop() 35 | coro = loop.create_server(ZenServer, *address) 36 | server = loop.run_until_complete(coro) 37 | print('Listening at {}'.format(address)) 38 | try: 39 | loop.run_forever() 40 | finally: 41 | server.close() 42 | loop.close() 43 | -------------------------------------------------------------------------------- /source/chapter07/srv_asyncio2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter07/srv_asyncio2.py 4 | # Asynchronous I/O inside an "asyncio" coroutine. 5 | 6 | import asyncio, zen_utils 7 | 8 | @asyncio.coroutine 9 | def handle_conversation(reader, writer): 10 | address = writer.get_extra_info('peername') 11 | print('Accepted connection from {}'.format(address)) 12 | while True: 13 | data = b'' 14 | while not data.endswith(b'?'): 15 | more_data = yield from reader.read(4096) 16 | if not more_data: 17 | if data: 18 | print('Client {} sent {!r} but then closed' 19 | .format(address, data)) 20 | else: 21 | print('Client {} closed socket normally'.format(address)) 22 | return 23 | data += more_data 24 | answer = zen_utils.get_answer(data) 25 | writer.write(answer) 26 | 27 | if __name__ == '__main__': 28 | address = zen_utils.parse_command_line('asyncio server using coroutine') 29 | loop = asyncio.get_event_loop() 30 | coro = asyncio.start_server(handle_conversation, *address) 31 | server = loop.run_until_complete(coro) 32 | print('Listening at {}'.format(address)) 33 | try: 34 | loop.run_forever() 35 | finally: 36 | server.close() 37 | loop.close() 38 | -------------------------------------------------------------------------------- /source/chapter07/srv_legacy1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter07/srv_legacy1.py 4 | # Uses the legacy "socketserver" Standard Library module to write a server. 5 | 6 | from socketserver import BaseRequestHandler, TCPServer, ThreadingMixIn 7 | import zen_utils 8 | 9 | class ZenHandler(BaseRequestHandler): 10 | def handle(self): 11 | zen_utils.handle_conversation(self.request, self.client_address) 12 | 13 | class ZenServer(ThreadingMixIn, TCPServer): 14 | allow_reuse_address = 1 15 | # address_family = socket.AF_INET6 # uncomment if you need IPv6 16 | 17 | if __name__ == '__main__': 18 | address = zen_utils.parse_command_line('legacy "SocketServer" server') 19 | server = ZenServer(address, ZenHandler) 20 | server.serve_forever() 21 | -------------------------------------------------------------------------------- /source/chapter07/srv_legacy2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter07/srv_legacy2.py 4 | # Uses the legacy "asyncore" Standard Library module to write a server. 5 | 6 | import asyncore, asynchat, zen_utils 7 | 8 | class ZenRequestHandler(asynchat.async_chat): 9 | 10 | def __init__(self, sock): 11 | asynchat.async_chat.__init__(self, sock) 12 | self.set_terminator(b'?') 13 | self.data = b'' 14 | 15 | def collect_incoming_data(self, more_data): 16 | self.data += more_data 17 | 18 | def found_terminator(self): 19 | answer = zen_utils.get_answer(self.data + b'?') 20 | self.push(answer) 21 | self.initiate_send() 22 | self.data = b'' 23 | 24 | class ZenServer(asyncore.dispatcher): 25 | 26 | def handle_accept(self): 27 | sock, address = self.accept() 28 | ZenRequestHandler(sock) 29 | 30 | if __name__ == '__main__': 31 | address = zen_utils.parse_command_line('legacy "asyncore" server') 32 | listener = zen_utils.create_srv_socket(address) 33 | server = ZenServer(listener) 34 | server.accepting = True # we already called listen() 35 | asyncore.loop() 36 | -------------------------------------------------------------------------------- /source/chapter07/srv_single.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter07/srv_single.py 4 | # Single-threaded server that serves one client at a time; others must wait. 5 | 6 | import zen_utils 7 | 8 | if __name__ == '__main__': 9 | address = zen_utils.parse_command_line('simple single-threaded server') 10 | listener = zen_utils.create_srv_socket(address) 11 | zen_utils.accept_connections_forever(listener) 12 | -------------------------------------------------------------------------------- /source/chapter07/srv_threaded.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter07/srv_threaded.py 4 | # Using multiple threads to serve several clients in parallel. 5 | 6 | import zen_utils 7 | from threading import Thread 8 | 9 | def start_threads(listener, workers=4): 10 | t = (listener,) 11 | for i in range(workers): 12 | Thread(target=zen_utils.accept_connections_forever, args=t).start() 13 | 14 | if __name__ == '__main__': 15 | address = zen_utils.parse_command_line('multi-threaded server') 16 | listener = zen_utils.create_srv_socket(address) 17 | start_threads(listener) 18 | -------------------------------------------------------------------------------- /source/chapter07/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | cd "$(dirname ${BASH_SOURCE[0]})" 5 | 6 | function runtest () { 7 | echo '===========' "$@" '===========' 8 | python3 "$@" & 9 | sleep 1 10 | python client.py localhost 11 | kill %1 12 | wait %1 2>/dev/null || true 13 | #kill $(lsof | grep 'TCP.*1060' | awk '{print$2}') 14 | } 15 | 16 | runtest srv_single.py localhost 17 | runtest srv_threaded.py localhost 18 | runtest srv_async.py localhost 19 | runtest srv_legacy1.py localhost 20 | runtest srv_legacy2.py localhost 21 | runtest srv_asyncio1.py localhost 22 | runtest srv_asyncio2.py localhost 23 | -------------------------------------------------------------------------------- /source/chapter07/trace.out: -------------------------------------------------------------------------------- 1 | # Excerpt from timing trace, showing that single-threaded server runtime 2 | # is dominated by waiting for the client. Captured with: 3 | # 4 | # python3.4 -m trace -tg --ignore-dir=/usr srv_single.py '' 5 | 6 | 3.02 zen_utils.py(40): print('Accepted connection from {}'.format(address)) 7 | Accepted connection from ('98.30.180.242', 40893) 8 | 3.02 zen_utils.py(41): handle_conversation(sock, address) 9 | --- modulename: zen_utils, funcname: handle_conversation 10 | 3.02 zen_utils.py(45): try: 11 | 3.02 zen_utils.py(46): while True: 12 | 3.02 zen_utils.py(47): handle_request(sock) 13 | --- modulename: zen_utils, funcname: handle_request 14 | 3.02 zen_utils.py(57): aphorism = recv_until(sock, b'?') 15 | --- modulename: zen_utils, funcname: recv_until 16 | 3.03 zen_utils.py(63): message = sock.recv(4096) 17 | 3.03 zen_utils.py(64): if not message: 18 | 3.03 zen_utils.py(66): while not message.endswith(suffix): 19 | 3.03 zen_utils.py(71): return message 20 | 3.03 zen_utils.py(58): answer = get_answer(aphorism) 21 | --- modulename: zen_utils, funcname: get_answer 22 | 3.03 zen_utils.py(14): time.sleep(0.0) # increase to simulate an expensive operation 23 | 3.03 zen_utils.py(15): return aphorisms.get(aphorism, b'Error: unknown aphorism.') 24 | 3.03 zen_utils.py(59): sock.sendall(answer) 25 | 3.03 zen_utils.py(47): handle_request(sock) 26 | --- modulename: zen_utils, funcname: handle_request 27 | 3.03 zen_utils.py(57): aphorism = recv_until(sock, b'?') 28 | --- modulename: zen_utils, funcname: recv_until 29 | 3.03 zen_utils.py(63): message = sock.recv(4096) 30 | 3.08 zen_utils.py(64): if not message: 31 | 3.08 zen_utils.py(66): while not message.endswith(suffix): 32 | 3.08 zen_utils.py(71): return message 33 | 3.08 zen_utils.py(58): answer = get_answer(aphorism) 34 | --- modulename: zen_utils, funcname: get_answer 35 | 3.08 zen_utils.py(14): time.sleep(0.0) # increase to simulate an expensive operation 36 | 3.08 zen_utils.py(15): return aphorisms.get(aphorism, b'Error: unknown aphorism.') 37 | 3.08 zen_utils.py(59): sock.sendall(answer) 38 | 3.08 zen_utils.py(47): handle_request(sock) 39 | --- modulename: zen_utils, funcname: handle_request 40 | 3.08 zen_utils.py(57): aphorism = recv_until(sock, b'?') 41 | --- modulename: zen_utils, funcname: recv_until 42 | 3.08 zen_utils.py(63): message = sock.recv(4096) 43 | 3.12 zen_utils.py(64): if not message: 44 | 3.12 zen_utils.py(66): while not message.endswith(suffix): 45 | 3.12 zen_utils.py(71): return message 46 | 3.12 zen_utils.py(58): answer = get_answer(aphorism) 47 | --- modulename: zen_utils, funcname: get_answer 48 | 3.12 zen_utils.py(14): time.sleep(0.0) # increase to simulate an expensive operation 49 | 3.12 zen_utils.py(15): return aphorisms.get(aphorism, b'Error: unknown aphorism.') 50 | 3.12 zen_utils.py(59): sock.sendall(answer) 51 | 3.12 zen_utils.py(47): handle_request(sock) 52 | --- modulename: zen_utils, funcname: handle_request 53 | 3.12 zen_utils.py(57): aphorism = recv_until(sock, b'?') 54 | --- modulename: zen_utils, funcname: recv_until 55 | 3.12 zen_utils.py(63): message = sock.recv(4096) 56 | 3.16 zen_utils.py(64): if not message: 57 | 3.16 zen_utils.py(65): raise EOFError('socket closed') 58 | 3.16 zen_utils.py(48): except EOFError: 59 | 3.16 zen_utils.py(49): print('Client socket to {} has closed'.format(address)) 60 | Client socket to ('98.30.180.242', 40893) has closed 61 | 3.16 zen_utils.py(53): sock.close() 62 | 3.16 zen_utils.py(39): sock, address = listener.accept() 63 | -------------------------------------------------------------------------------- /source/chapter07/zen_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter07/zen_utils.py 4 | # Constants and routines for supporting a certain network conversation. 5 | 6 | import argparse, socket, time 7 | 8 | aphorisms = {b'Beautiful is better than?': b'Ugly.', 9 | b'Explicit is better than?': b'Implicit.', 10 | b'Simple is better than?': b'Complex.'} 11 | 12 | def get_answer(aphorism): 13 | """Return the string response to a particular Zen-of-Python aphorism.""" 14 | time.sleep(0.0) # increase to simulate an expensive operation 15 | return aphorisms.get(aphorism, b'Error: unknown aphorism.') 16 | 17 | def parse_command_line(description): 18 | """Parse command line and return a socket address.""" 19 | parser = argparse.ArgumentParser(description=description) 20 | parser.add_argument('host', help='IP or hostname') 21 | parser.add_argument('-p', metavar='port', type=int, default=1060, 22 | help='TCP port (default 1060)') 23 | args = parser.parse_args() 24 | address = (args.host, args.p) 25 | return address 26 | 27 | def create_srv_socket(address): 28 | """Build and return a listening server socket.""" 29 | listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 30 | listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 31 | listener.bind(address) 32 | listener.listen(64) 33 | print('Listening at {}'.format(address)) 34 | return listener 35 | 36 | def accept_connections_forever(listener): 37 | """Forever answer incoming connections on a listening socket.""" 38 | while True: 39 | sock, address = listener.accept() 40 | print('Accepted connection from {}'.format(address)) 41 | handle_conversation(sock, address) 42 | 43 | def handle_conversation(sock, address): 44 | """Converse with a client over `sock` until they are done talking.""" 45 | try: 46 | while True: 47 | handle_request(sock) 48 | except EOFError: 49 | print('Client socket to {} has closed'.format(address)) 50 | except Exception as e: 51 | print('Client {} error: {}'.format(address, e)) 52 | finally: 53 | sock.close() 54 | 55 | def handle_request(sock): 56 | """Receive a single client request on `sock` and send the answer.""" 57 | aphorism = recv_until(sock, b'?') 58 | answer = get_answer(aphorism) 59 | sock.sendall(answer) 60 | 61 | def recv_until(sock, suffix): 62 | """Receive bytes over socket `sock` until we receive the `suffix`.""" 63 | message = sock.recv(4096) 64 | if not message: 65 | raise EOFError('socket closed') 66 | while not message.endswith(suffix): 67 | data = sock.recv(4096) 68 | if not data: 69 | raise IOError('received {!r} then socket closed'.format(message)) 70 | message += data 71 | return message 72 | -------------------------------------------------------------------------------- /source/chapter08/README.md: -------------------------------------------------------------------------------- 1 | [Return to the Table of Contents](https://github.com/brandon-rhodes/fopnp#readme) 2 | 3 | # Chapter 8
Caches and Message Queues 4 | 5 | This is a directory of program listings from Chapter 8 of the book: 6 | 7 |
8 |
Foundations of Python Network Programming
9 |
10 | Third Edition, October 2014
11 | by Brandon Rhodes and John Goerzen 12 |
13 |
14 | 15 | You can learn more about the book by visiting the 16 | [root of this GitHub source code repository](https://github.com/brandon-rhodes/fopnp#readme). 17 | 18 | The scripts here in Chapter 8 can run into a few snags if you try 19 | converting them to Python 2, because of differences in how both the 20 | `hashlib` Standard Library module and the third-party `memcache` package 21 | treat bytes and strings differently under Python 2 versus Python 3. If 22 | you are still using Python 2, try consulting the three scripts from 23 | Chapter 8 in the [previous edition of the book.](https://github.com/brandon-rhodes/fopnp/tree/m/py2/chapter08) 24 | 25 | The three scripts in this chapter power its discussion of caches and 26 | message queues — network services which are fundamental to how modern 27 | services scale out to very large numbers of clients. 28 | 29 | The `squares.py` script requires you to install `memcached` on your 30 | system, along with a Python library for communicating with it — which 31 | should already be available if you have installed everything in this 32 | repository’s [`requirements.txt`](https://github.com/brandon-rhodes/fopnp/blob/m/py3/requirements.txt). 33 | 34 | ``` 35 | $ python3 squares.py 36 | Ten successive runs: 37 | 3.01s 2.14s 1.61s 1.16s 0.90s 0.70s 0.57s 0.50s 0.43s 0.37s 38 | ``` 39 | 40 | The `hashing.py` script needs only the Standard Library. 41 | 42 | ``` 43 | $ python3 hashing.py 44 | alpha 45 | server0 35285 0.36 46 | server1 22674 0.23 47 | server2 29097 0.29 48 | server3 12115 0.12 49 | 50 | hash 51 | server0 24748 0.25 52 | server1 24743 0.25 53 | server2 24943 0.25 54 | server3 24737 0.25 55 | 56 | md5 57 | server0 24777 0.25 58 | server1 24820 0.25 59 | server2 24717 0.25 60 | server3 24857 0.25 61 | 62 | ``` 63 | 64 | The `queuepi.py` script instead requires `pyzmq` in order to run, but 65 | does not require any central message queue to be running. It uses a 66 | rather abstruse set of cooperating workers. 67 | 68 | ``` 69 | $ python3 queuepi.py 70 | Y 4.0 71 | Y 4.0 72 | Y 4.0 73 | Y 4.0 74 | Y 4.0 75 | N 3.3333333333333335 76 | N 2.857142857142857 77 | N 2.5 78 | N 2.2222222222222223 79 | N 2.0 80 | . 81 | . 82 | . 83 | Y 3.1297185998627315 84 | Y 3.130017152658662 85 | N 3.128943758573388 86 | Y 3.1292423723003084 87 | Y 3.1295407813570937 88 | Y 3.129838985954094 89 | Y 3.1301369863013697 90 | N 3.12906538856556 91 | N 3.1279945242984257 92 | Y 3.128292849811837 93 | ``` 94 | -------------------------------------------------------------------------------- /source/chapter08/hashing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter08/hashing.py 4 | # Hashes are a great way to divide work. 5 | 6 | import hashlib 7 | 8 | def alpha_shard(word): 9 | """Do a poor job of assigning data to servers by using first letters.""" 10 | if word[0] < 'g': # abcdef 11 | return 'server0' 12 | elif word[0] < 'n': # ghijklm 13 | return 'server1' 14 | elif word[0] < 't': # nopqrs 15 | return 'server2' 16 | else: # tuvwxyz 17 | return 'server3' 18 | 19 | def hash_shard(word): 20 | """Assign data to servers using Python's built-in hash() function.""" 21 | return 'server%d' % (hash(word) % 4) 22 | 23 | def md5_shard(word): 24 | """Assign data to servers using a public hash algorithm.""" 25 | data = word.encode('utf-8') 26 | return 'server%d' % (hashlib.md5(data).digest()[-1] % 4) 27 | 28 | if __name__ == '__main__': 29 | words = open('/usr/share/dict/words').read().split() 30 | for function in alpha_shard, hash_shard, md5_shard: 31 | d = {'server0': 0, 'server1': 0, 'server2': 0, 'server3': 0} 32 | for word in words: 33 | d[function(word.lower())] += 1 34 | print(function.__name__[:-6]) 35 | for key, value in sorted(d.items()): 36 | print(' {} {} {:.2}'.format(key, value, value / len(words))) 37 | print() 38 | -------------------------------------------------------------------------------- /source/chapter08/queuecrazy.py: -------------------------------------------------------------------------------- 1 | # The script printed in the book that calls itself "queuecrazy.py" is 2 | # actually named "queuepi.py", as you can see a page or two later when 3 | # the book demonstrates how to invoke the script at the shell prompt. 4 | # So the script actually lives at the URL: 5 | 6 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter08/queuepi.py 7 | 8 | # Sorry about that! 9 | -------------------------------------------------------------------------------- /source/chapter08/queuepi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter08/queuepi.py 4 | # Small application that uses several different message queues 5 | 6 | import random, threading, time, zmq 7 | B = 32 # number of bits of precision in each random integer 8 | 9 | def ones_and_zeros(digits): 10 | """Express `n` in at least `d` binary digits, with no special prefix.""" 11 | return bin(random.getrandbits(digits)).lstrip('0b').zfill(digits) 12 | 13 | def bitsource(zcontext, url): 14 | """Produce random points in the unit square.""" 15 | zsock = zcontext.socket(zmq.PUB) 16 | zsock.bind(url) 17 | while True: 18 | zsock.send_string(ones_and_zeros(B * 2)) 19 | time.sleep(0.01) 20 | 21 | def always_yes(zcontext, in_url, out_url): 22 | """Coordinates in the lower-left quadrant are inside the unit circle.""" 23 | isock = zcontext.socket(zmq.SUB) 24 | isock.connect(in_url) 25 | isock.setsockopt(zmq.SUBSCRIBE, b'00') 26 | osock = zcontext.socket(zmq.PUSH) 27 | osock.connect(out_url) 28 | while True: 29 | isock.recv_string() 30 | osock.send_string('Y') 31 | 32 | def judge(zcontext, in_url, pythagoras_url, out_url): 33 | """Determine whether each input coordinate is inside the unit circle.""" 34 | isock = zcontext.socket(zmq.SUB) 35 | isock.connect(in_url) 36 | for prefix in b'01', b'10', b'11': 37 | isock.setsockopt(zmq.SUBSCRIBE, prefix) 38 | psock = zcontext.socket(zmq.REQ) 39 | psock.connect(pythagoras_url) 40 | osock = zcontext.socket(zmq.PUSH) 41 | osock.connect(out_url) 42 | unit = 2 ** (B * 2) 43 | while True: 44 | bits = isock.recv_string() 45 | n, m = int(bits[::2], 2), int(bits[1::2], 2) 46 | psock.send_json((n, m)) 47 | sumsquares = psock.recv_json() 48 | osock.send_string('Y' if sumsquares < unit else 'N') 49 | 50 | def pythagoras(zcontext, url): 51 | """Return the sum-of-squares of number sequences.""" 52 | zsock = zcontext.socket(zmq.REP) 53 | zsock.bind(url) 54 | while True: 55 | numbers = zsock.recv_json() 56 | zsock.send_json(sum(n * n for n in numbers)) 57 | 58 | def tally(zcontext, url): 59 | """Tally how many points fall within the unit circle, and print pi.""" 60 | zsock = zcontext.socket(zmq.PULL) 61 | zsock.bind(url) 62 | p = q = 0 63 | while True: 64 | decision = zsock.recv_string() 65 | q += 1 66 | if decision == 'Y': 67 | p += 4 68 | print(decision, p / q) 69 | 70 | def start_thread(function, *args): 71 | thread = threading.Thread(target=function, args=args) 72 | thread.daemon = True # so you can easily Ctrl-C the whole program 73 | thread.start() 74 | 75 | def main(zcontext): 76 | pubsub = 'tcp://127.0.0.1:6700' 77 | reqrep = 'tcp://127.0.0.1:6701' 78 | pushpull = 'tcp://127.0.0.1:6702' 79 | start_thread(bitsource, zcontext, pubsub) 80 | start_thread(always_yes, zcontext, pubsub, pushpull) 81 | start_thread(judge, zcontext, pubsub, reqrep, pushpull) 82 | start_thread(pythagoras, zcontext, reqrep) 83 | start_thread(tally, zcontext, pushpull) 84 | time.sleep(30) 85 | 86 | if __name__ == '__main__': 87 | main(zmq.Context()) 88 | -------------------------------------------------------------------------------- /source/chapter08/squares.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter08/squares.py 4 | # Using memcached to cache expensive results. 5 | 6 | import memcache, random, time, timeit 7 | 8 | def compute_square(mc, n): 9 | value = mc.get('sq:%d' % n) 10 | if value is None: 11 | time.sleep(0.001) # pretend that computing a square is expensive 12 | value = n * n 13 | mc.set('sq:%d' % n, value) 14 | return value 15 | 16 | def main(): 17 | mc = memcache.Client(['127.0.0.1:11211']) 18 | 19 | def make_request(): 20 | compute_square(mc, random.randint(0, 5000)) 21 | 22 | print('Ten successive runs:') 23 | for i in range(1, 11): 24 | print(' %.2fs' % timeit.timeit(make_request, number=2000), end='') 25 | print() 26 | 27 | if __name__ == '__main__': 28 | main() 29 | -------------------------------------------------------------------------------- /source/chapter09/README.md: -------------------------------------------------------------------------------- 1 | [Return to the Table of Contents](https://github.com/brandon-rhodes/fopnp#readme) 2 | 3 | # Chapter 9
HTTP Clients 4 | 5 | This is a directory of program listings from Chapter 9 of the book: 6 | 7 |
8 |
Foundations of Python Network Programming
9 |
10 | Third Edition, October 2014
11 | by Brandon Rhodes and John Goerzen 12 |
13 |
14 | 15 | You can learn more about the book by visiting the 16 | [root of this GitHub source code repository](https://github.com/brandon-rhodes/fopnp#readme). 17 | 18 | Chapter 9 is a detailed tour of HTTP and its features, illuminated by a 19 | series of live examples at the Python prompt. The examples are 20 | collected here in the `examples.doctest` which you can run on your own 21 | system if you first install `gunicorn` and `httpbin` and then run them 22 | using the command listed at the top of `config.py`: 23 | 24 | $ gunicorn -c config.py httpbin:app 25 | 26 | The doctest file can then be exercised, if you have `requests` 27 | installed, with: 28 | 29 | $ python3 -m doctest examples.doctest 30 | 31 | All three of these dependencies will be installed automatically if you 32 | install everything in [`requirements.txt`](https://github.com/brandon-rhodes/fopnp/blob/m/py3/requirements.txt). 33 | 34 | If you are interested in trying out these examples under Python 2, you 35 | will find that the code that uses `requests` will usually work without 36 | any change, while all of the code that needs `urllib` or `http` from the 37 | Standard Library will need to be reworked to use the old names for those 38 | libraries instead. Running [3to2](https://pypi.python.org/pypi/3to2) on 39 | any of the example code, once you have pasted it into a plain `.py` 40 | file, should perform the renaming needed to get the code ready to run 41 | under Python 2. 42 | -------------------------------------------------------------------------------- /source/chapter09/config.py: -------------------------------------------------------------------------------- 1 | # A Gunicorn configuration file that I used during the writing of 2 | # Foundations of Python Network Programming, Third Edition, to print out 3 | # various HTTP requests and responses. 4 | # - Brandon Rhodes 5 | 6 | """Gunicorn configuration that prints HTTP to the screen. 7 | 8 | To use this Gunicorn configuration, which prints HTTP requests and 9 | responses to the screen, with the httpbin application, run the 10 | following command in this directory after pip installing both 11 | "gunicorn" and "httpbin" under Python 3: 12 | 13 | gunicorn -c config.py httpbin:app 14 | 15 | """ 16 | workers = 1 17 | worker_class = 'sync' 18 | 19 | def printout(data): 20 | """Print and then return the given data.""" 21 | print(data.decode('utf-8')) 22 | return data 23 | 24 | class Noisy: 25 | def __init__(self, sock): self.sock = sock 26 | def recv(self, count): return printout(self.sock.recv(count)) 27 | def send(self, data): return self.sock.send(printout(data)) 28 | def sendall(self, data): return self.sock.sendall(printout(data)) 29 | def __getattr__(self, name): return getattr(self.sock, name) 30 | 31 | def post_fork(server, worker): 32 | def accept(): 33 | client, addr = _accept() 34 | return Noisy(client), addr 35 | sock = worker.sockets[0] 36 | _accept = sock.accept 37 | sock.accept = accept 38 | -------------------------------------------------------------------------------- /source/chapter10/_test.py: -------------------------------------------------------------------------------- 1 | """Check that a server works. 2 | 3 | Run the service under test (which should be another Python module in 4 | this directory) like: 5 | 6 | $ gunicorn -b '' timeapp_raw:app 7 | 8 | """ 9 | import requests 10 | 11 | def display(r): 12 | print((r.status_code, r.headers['Content-Type'], r.text)) 13 | 14 | if __name__ == '__main__': 15 | 16 | # Wrong method 17 | 18 | r = requests.post('http://localhost:8000/') 19 | display(r) 20 | assert r.status_code == 501 21 | 22 | # Wrong hostname 23 | 24 | r = requests.get('http://localhost:8000/') 25 | display(r) 26 | assert r.status_code == 404 27 | 28 | # Wrong path 29 | 30 | r = requests.get('http://127.0.0.1:8000/foo') 31 | display(r) 32 | assert r.status_code == 404 33 | 34 | r = requests.get('http://127.0.0.1:8000/foo/') 35 | display(r) 36 | assert r.status_code == 404 37 | 38 | # Right path 39 | 40 | r = requests.get('http://127.0.0.1:8000/') 41 | display(r) 42 | assert r.status_code == 200 43 | 44 | r = requests.get('http://127.0.0.1:8000/?abc=123') 45 | display(r) 46 | assert r.status_code == 200 47 | -------------------------------------------------------------------------------- /source/chapter10/timeapp_raw.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter10/timeapp_raw.py 4 | # A simple HTTP service built directly against the low-level WSGI spec. 5 | 6 | import time 7 | 8 | def app(environ, start_response): 9 | host = environ.get('HTTP_HOST', '127.0.0.1') 10 | path = environ.get('PATH_INFO', '/') 11 | if ':' in host: 12 | host, port = host.split(':', 1) 13 | if '?' in path: 14 | path, query = path.split('?', 1) 15 | headers = [('Content-Type', 'text/plain; charset=utf-8')] 16 | if environ['REQUEST_METHOD'] != 'GET': 17 | start_response('501 Not Implemented', headers) 18 | yield b'501 Not Implemented' 19 | elif host != '127.0.0.1' or path != '/': 20 | start_response('404 Not Found', headers) 21 | yield b'404 Not Found' 22 | else: 23 | start_response('200 OK', headers) 24 | yield time.ctime().encode('ascii') 25 | -------------------------------------------------------------------------------- /source/chapter10/timeapp_webob.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter10/timeapp_webob.py 4 | # A WSGI callable built using webob. 5 | 6 | import time, webob 7 | 8 | def app(environ, start_response): 9 | request = webob.Request(environ) 10 | if environ['REQUEST_METHOD'] != 'GET': 11 | response = webob.Response('501 Not Implemented', status=501) 12 | elif request.domain != '127.0.0.1' or request.path != '/': 13 | response = webob.Response('404 Not Found', status=404) 14 | else: 15 | response = webob.Response(time.ctime()) 16 | return response(environ, start_response) 17 | -------------------------------------------------------------------------------- /source/chapter10/timeapp_werkz.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter10/timeapp_werkz.py 4 | # A WSGI callable built using Werkzeug. 5 | 6 | import time 7 | from werkzeug.wrappers import Request, Response 8 | 9 | @Request.application 10 | def app(request): 11 | host = request.host 12 | if ':' in host: 13 | host, port = host.split(':', 1) 14 | if request.method != 'GET': 15 | return Response('501 Not Implemented', status=501) 16 | elif host != '127.0.0.1' or request.path != '/': 17 | return Response('404 Not Found', status=404) 18 | else: 19 | return Response(time.ctime()) 20 | -------------------------------------------------------------------------------- /source/chapter10/wsgi_env.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter10/wsgi_env.py 4 | # A simple HTTP service built directly against the low-level WSGI spec. 5 | 6 | from pprint import pformat 7 | from wsgiref.simple_server import make_server 8 | 9 | def app(environ, start_response): 10 | headers = {'Content-Type': 'text/plain; charset=utf-8'} 11 | start_response('200 OK', list(headers.items())) 12 | yield 'Here is the WSGI environment:\r\n\r\n'.encode('utf-8') 13 | yield pformat(environ).encode('utf-8') 14 | 15 | if __name__ == '__main__': 16 | httpd = make_server('', 8000, app) 17 | host, port = httpd.socket.getsockname() 18 | print('Serving on', host, 'port', port) 19 | httpd.serve_forever() 20 | -------------------------------------------------------------------------------- /source/chapter11/app_improved.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter11/app_improved.py 4 | # A payments application with basic security improvements added. 5 | 6 | import bank, uuid 7 | from flask import (Flask, abort, flash, get_flashed_messages, 8 | redirect, render_template, request, session, url_for) 9 | 10 | app = Flask(__name__) 11 | app.secret_key = 'saiGeij8AiS2ahleahMo5dahveixuV3J' 12 | 13 | @app.route('/login', methods=['GET', 'POST']) 14 | def login(): 15 | username = request.form.get('username', '') 16 | password = request.form.get('password', '') 17 | if request.method == 'POST': 18 | if (username, password) in [('brandon', 'atigdng'), ('sam', 'xyzzy')]: 19 | session['username'] = username 20 | session['csrf_token'] = uuid.uuid4().hex 21 | return redirect(url_for('index')) 22 | return render_template('login.html', username=username) 23 | 24 | @app.route('/logout') 25 | def logout(): 26 | session.pop('username', None) 27 | return redirect(url_for('login')) 28 | 29 | @app.route('/') 30 | def index(): 31 | username = session.get('username') 32 | if not username: 33 | return redirect(url_for('login')) 34 | payments = bank.get_payments_of(bank.open_database(), username) 35 | return render_template('index.html', payments=payments, username=username, 36 | flash_messages=get_flashed_messages()) 37 | 38 | @app.route('/pay', methods=['GET', 'POST']) 39 | def pay(): 40 | username = session.get('username') 41 | if not username: 42 | return redirect(url_for('login')) 43 | account = request.form.get('account', '').strip() 44 | dollars = request.form.get('dollars', '').strip() 45 | memo = request.form.get('memo', '').strip() 46 | complaint = None 47 | if request.method == 'POST': 48 | if request.form.get('csrf_token') != session['csrf_token']: 49 | abort(403) 50 | if account and dollars and dollars.isdigit() and memo: 51 | db = bank.open_database() 52 | bank.add_payment(db, username, account, dollars, memo) 53 | db.commit() 54 | flash('Payment successful') 55 | return redirect(url_for('index')) 56 | complaint = ('Dollars must be an integer' if not dollars.isdigit() 57 | else 'Please fill in all three fields') 58 | return render_template('pay2.html', complaint=complaint, account=account, 59 | dollars=dollars, memo=memo, 60 | csrf_token=session['csrf_token']) 61 | 62 | if __name__ == '__main__': 63 | app.debug = True 64 | app.run() 65 | -------------------------------------------------------------------------------- /source/chapter11/app_insecure.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter11/app_insecure.py 4 | # A poorly-written and profoundly insecure payments application. 5 | # (Not the fault of Flask, but of how we are choosing to use it!) 6 | 7 | import bank 8 | from flask import Flask, redirect, request, url_for 9 | from jinja2 import Environment, PackageLoader 10 | 11 | app = Flask(__name__) 12 | get = Environment(loader=PackageLoader(__name__, 'templates')).get_template 13 | 14 | @app.route('/login', methods=['GET', 'POST']) 15 | def login(): 16 | username = request.form.get('username', '') 17 | password = request.form.get('password', '') 18 | if request.method == 'POST': 19 | if (username, password) in [('brandon', 'atigdng'), ('sam', 'xyzzy')]: 20 | response = redirect(url_for('index')) 21 | response.set_cookie('username', username) 22 | return response 23 | return get('login.html').render(username=username) 24 | 25 | @app.route('/logout') 26 | def logout(): 27 | response = redirect(url_for('login')) 28 | response.set_cookie('username', '') 29 | return response 30 | 31 | @app.route('/') 32 | def index(): 33 | username = request.cookies.get('username') 34 | if not username: 35 | return redirect(url_for('login')) 36 | payments = bank.get_payments_of(bank.open_database(), username) 37 | return get('index.html').render(payments=payments, username=username, 38 | flash_messages=request.args.getlist('flash')) 39 | 40 | @app.route('/pay', methods=['GET', 'POST']) 41 | def pay(): 42 | username = request.cookies.get('username') 43 | if not username: 44 | return redirect(url_for('login')) 45 | account = request.form.get('account', '').strip() 46 | dollars = request.form.get('dollars', '').strip() 47 | memo = request.form.get('memo', '').strip() 48 | complaint = None 49 | if request.method == 'POST': 50 | if account and dollars and dollars.isdigit() and memo: 51 | db = bank.open_database() 52 | bank.add_payment(db, username, account, dollars, memo) 53 | db.commit() 54 | return redirect(url_for('index', flash='Payment successful')) 55 | complaint = ('Dollars must be an integer' if not dollars.isdigit() 56 | else 'Please fill in all three fields') 57 | return get('pay.html').render(complaint=complaint, account=account, 58 | dollars=dollars, memo=memo) 59 | 60 | if __name__ == '__main__': 61 | app.debug = True 62 | app.run() 63 | -------------------------------------------------------------------------------- /source/chapter11/attack.js: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /source/chapter11/bank.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter11/bank.py 4 | # A small library of database routines to power a payments application. 5 | 6 | import os, pprint, sqlite3 7 | from collections import namedtuple 8 | 9 | def open_database(path='bank.db'): 10 | new = not os.path.exists(path) 11 | db = sqlite3.connect(path) 12 | if new: 13 | c = db.cursor() 14 | c.execute('CREATE TABLE payment (id INTEGER PRIMARY KEY,' 15 | ' debit TEXT, credit TEXT, dollars INTEGER, memo TEXT)') 16 | add_payment(db, 'brandon', 'psf', 125, 'Registration for PyCon') 17 | add_payment(db, 'brandon', 'liz', 200, 'Payment for writing that code') 18 | add_payment(db, 'sam', 'brandon', 25, 'Gas money-thanks for the ride!') 19 | db.commit() 20 | return db 21 | 22 | def add_payment(db, debit, credit, dollars, memo): 23 | db.cursor().execute('INSERT INTO payment (debit, credit, dollars, memo)' 24 | ' VALUES (?, ?, ?, ?)', (debit, credit, dollars, memo)) 25 | 26 | def get_payments_of(db, account): 27 | c = db.cursor() 28 | c.execute('SELECT * FROM payment WHERE credit = ? or debit = ?' 29 | ' ORDER BY id', (account, account)) 30 | Row = namedtuple('Row', [tup[0] for tup in c.description]) 31 | return [Row(*row) for row in c.fetchall()] 32 | 33 | if __name__ == '__main__': 34 | db = open_database() 35 | pprint.pprint(get_payments_of(db, 'brandon')) 36 | -------------------------------------------------------------------------------- /source/chapter11/csrf.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Win Big! 4 | 5 | 6 |

All you have to do is press the button.

7 |
8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /source/chapter11/csrf_auto.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Win Big! 4 | 5 | 6 |

You are already a winner!

7 |
8 | 9 | 10 | 11 | 12 |
13 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /source/chapter11/djbank/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/foundations-of-python-network-programming-14/742320235e7a62854b7cbb4b21a4b1b3c6e07c08/source/chapter11/djbank/__init__.py -------------------------------------------------------------------------------- /source/chapter11/djbank/admin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter11/djbank/admin.py 4 | # Admin site setup for our Django application. 5 | 6 | from django.contrib import admin 7 | from .models import Payment 8 | 9 | admin.site.register(Payment) 10 | -------------------------------------------------------------------------------- /source/chapter11/djbank/fixtures/start.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "djbank.payment", "pk": 1, "fields": { 4 | "debit": "brandon", "credit": "psf", "dollars": 125, 5 | "memo": "Registration for PyCon" 6 | } 7 | }, 8 | { 9 | "model": "djbank.payment", "pk": 2, "fields": { 10 | "debit": "brandon", "credit": "liz", "dollars": 200, 11 | "memo": "Payment for writing that code" 12 | } 13 | }, 14 | { 15 | "model": "djbank.payment", "pk": 3, "fields": { 16 | "debit": "sam", "credit": "brandon", "dollars": 25, 17 | "memo": "Gas money-thanks for the ride!" 18 | } 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /source/chapter11/djbank/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter11/djbank/models.py 4 | # Model definitions for our Django application. 5 | 6 | from django.db import models 7 | from django.forms import ModelForm 8 | 9 | class Payment(models.Model): 10 | debit = models.CharField(max_length=200) 11 | credit = models.CharField(max_length=200, verbose_name='To account') 12 | dollars = models.PositiveIntegerField() 13 | memo = models.CharField(max_length=200) 14 | 15 | class PaymentForm(ModelForm): 16 | class Meta: 17 | model = Payment 18 | fields = ['credit', 'dollars', 'memo'] 19 | -------------------------------------------------------------------------------- /source/chapter11/djbank/settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter11/djbank/settings.py 4 | # Settings file for our Django application (see addition at bottom). 5 | # ---------------------------------------------------------------------- 6 | """ 7 | Django settings for djbank project. 8 | 9 | For more information on this file, see 10 | https://docs.djangoproject.com/en/1.7/topics/settings/ 11 | 12 | For the full list of settings and their values, see 13 | https://docs.djangoproject.com/en/1.7/ref/settings/ 14 | """ 15 | 16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 17 | import os 18 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 19 | 20 | 21 | # Quick-start development settings - unsuitable for production 22 | # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ 23 | 24 | # SECURITY WARNING: keep the secret key used in production secret! 25 | SECRET_KEY = 'iwz)*5w!=$zx%^fm!7tx=mr5zr_ggoa$^pan@2!7-!2!jkcfew' 26 | 27 | # SECURITY WARNING: don't run with debug turned on in production! 28 | DEBUG = True 29 | 30 | TEMPLATE_DEBUG = True 31 | 32 | ALLOWED_HOSTS = [] 33 | 34 | 35 | # Application definition 36 | 37 | INSTALLED_APPS = ( 38 | 'django.contrib.admin', 39 | 'django.contrib.auth', 40 | 'django.contrib.contenttypes', 41 | 'django.contrib.sessions', 42 | 'django.contrib.messages', 43 | 'django.contrib.staticfiles', 44 | 'djbank', 45 | ) 46 | 47 | MIDDLEWARE_CLASSES = ( 48 | 'django.contrib.sessions.middleware.SessionMiddleware', 49 | 'django.middleware.common.CommonMiddleware', 50 | 'django.middleware.csrf.CsrfViewMiddleware', 51 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 52 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 53 | 'django.contrib.messages.middleware.MessageMiddleware', 54 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 55 | ) 56 | 57 | ROOT_URLCONF = 'djbank.urls' 58 | 59 | WSGI_APPLICATION = 'djbank.wsgi.application' 60 | 61 | 62 | # Database 63 | # https://docs.djangoproject.com/en/1.7/ref/settings/#databases 64 | 65 | DATABASES = { 66 | 'default': { 67 | 'ENGINE': 'django.db.backends.sqlite3', 68 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 69 | } 70 | } 71 | 72 | # Internationalization 73 | # https://docs.djangoproject.com/en/1.7/topics/i18n/ 74 | 75 | LANGUAGE_CODE = 'en-us' 76 | 77 | TIME_ZONE = 'UTC' 78 | 79 | USE_I18N = True 80 | 81 | USE_L10N = True 82 | 83 | USE_TZ = True 84 | 85 | 86 | # Static files (CSS, JavaScript, Images) 87 | # https://docs.djangoproject.com/en/1.7/howto/static-files/ 88 | 89 | STATIC_URL = '/static/' 90 | 91 | # ---------------------------------------------------------------------- 92 | # All of the above are the pristine settings as written by the 93 | # "startapp" command. Here are the additions necessary for the app as 94 | # published in Foundations of Python Network Programming: 95 | 96 | STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'),) 97 | -------------------------------------------------------------------------------- /source/chapter11/djbank/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% block title %}{% endblock %} 4 | 5 | 6 | 7 |

{% block title2 %}{% endblock %}

8 | {% block content %}{% endblock %} 9 | 10 | 11 | -------------------------------------------------------------------------------- /source/chapter11/djbank/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}{% block title2 %}Welcome, {{ user.username }} 3 | {% endblock %}{% endblock %} 4 | {% block content %} 5 | {% for message in messages %} 6 |
{{ message }}×
7 | {% endfor %} 8 |

Your Payments

9 |
    10 | {% for p in payments %} 11 |
  • ${{ p.dollars }} {{ p.prep }} 12 | {{ p.account }} for: {{ p.memo }}
  • 13 | {% endfor %} 14 |
15 | Make payment | Log out 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /source/chapter11/djbank/templates/pay.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}{% block title2 %}Make a Payment{% endblock %}{% endblock %} 3 | {% block content %} 4 |
5 | {{ form.as_p }} 6 | {% csrf_token %} 7 | | Cancel 8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /source/chapter11/djbank/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% extends "base.html" %} 4 | 5 | {% block content %} 6 | 7 | {% if form.errors %} 8 |

Your username and password didn't match. Please try again.

9 | {% endif %} 10 | 11 |
12 | {% csrf_token %} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
23 | 24 | 25 | 26 |
27 | 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /source/chapter11/djbank/urls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter11/djbank/urls.py 4 | # URL patterns for our Django application. 5 | 6 | from django.conf.urls import patterns, include, url 7 | from django.contrib import admin 8 | from django.contrib.auth.views import login 9 | 10 | urlpatterns = patterns('', 11 | url(r'^admin/', include(admin.site.urls)), 12 | url(r'^accounts/login/$', login), 13 | url(r'^$', 'djbank.views.index_view', name='index'), 14 | url(r'^pay/$', 'djbank.views.pay_view', name='pay'), 15 | url(r'^logout/$', 'djbank.views.logout_view'), 16 | ) 17 | -------------------------------------------------------------------------------- /source/chapter11/djbank/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter11/djbank/views.py 4 | # A function for each view in our Django application. 5 | 6 | from django.contrib import messages 7 | from django.contrib.auth.decorators import login_required 8 | from django.contrib.auth import logout 9 | from django.db.models import Q 10 | from django.shortcuts import redirect, render 11 | from django.views.decorators.http import require_http_methods, require_safe 12 | from .models import Payment, PaymentForm 13 | 14 | def make_payment_views(payments, username): 15 | for p in payments: 16 | yield {'dollars': p.dollars, 'memo': p.memo, 17 | 'prep': 'to' if (p.debit == username) else 'from', 18 | 'account': p.credit if (p.debit == username) else p.debit} 19 | 20 | @require_safe 21 | @login_required 22 | def index_view(request): 23 | username = request.user.username 24 | payments = Payment.objects.filter(Q(credit=username) | Q(debit=username)) 25 | payment_views = make_payment_views(payments, username) 26 | return render(request, 'index.html', {'payments': payment_views}) 27 | 28 | @require_http_methods(['GET', 'POST']) 29 | @login_required 30 | def pay_view(request): 31 | form = PaymentForm(request.POST or None) 32 | if form.is_valid(): 33 | payment = form.save(commit=False) 34 | payment.debit = request.user.username 35 | payment.save() 36 | messages.add_message(request, messages.INFO, 'Payment successful.') 37 | return redirect('/') 38 | return render(request, 'pay.html', {'form': form}) 39 | 40 | @require_safe 41 | def logout_view(request): 42 | logout(request) 43 | return redirect('/') 44 | -------------------------------------------------------------------------------- /source/chapter11/djbank/wsgi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter11/djbank/wsgi.py 4 | # Standard, unchanged WSGI callable produced by Django. 5 | # ---------------------------------------------------------------------- 6 | """ 7 | WSGI config for djbank project. 8 | 9 | It exposes the WSGI callable as a module-level variable named ``application``. 10 | 11 | For more information on this file, see 12 | https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ 13 | """ 14 | 15 | import os 16 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djbank.settings") 17 | 18 | from django.core.wsgi import get_wsgi_application 19 | application = get_wsgi_application() 20 | -------------------------------------------------------------------------------- /source/chapter11/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter11/manage.py 4 | # The "manage.py" file produced by Django for the "djbank" application: 5 | # ---------------------------------------------------------------------- 6 | #!/usr/bin/env python 7 | import os 8 | import sys 9 | 10 | if __name__ == "__main__": 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djbank.settings") 12 | 13 | from django.core.management import execute_from_command_line 14 | 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /source/chapter11/mscrape.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter11/mscrape.py 4 | # Manual scraping, that navigates to a particular page and grabs data. 5 | 6 | import argparse, bs4, lxml.html, requests 7 | from selenium import webdriver 8 | from urllib.parse import urljoin 9 | 10 | ROW = '{:>12} {}' 11 | 12 | def download_page_with_requests(base): 13 | session = requests.Session() 14 | response = session.post(urljoin(base, '/login'), 15 | {'username': 'brandon', 'password': 'atigdng'}) 16 | assert response.url == urljoin(base, '/') 17 | return response.text 18 | 19 | def download_page_with_selenium(base): 20 | browser = webdriver.Firefox() 21 | browser.get(base) 22 | assert browser.current_url == urljoin(base, '/login') 23 | css = browser.find_element_by_css_selector 24 | css('input[name="username"]').send_keys('brandon') 25 | css('input[name="password"]').send_keys('atigdng') 26 | css('input[name="password"]').submit() 27 | assert browser.current_url == urljoin(base, '/') 28 | return browser.page_source 29 | 30 | def scrape_with_soup(text): 31 | soup = bs4.BeautifulSoup(text) 32 | total = 0 33 | for li in soup.find_all('li', 'to'): 34 | dollars = int(li.get_text().split()[0].lstrip('$')) 35 | memo = li.find('i').get_text() 36 | total += dollars 37 | print(ROW.format(dollars, memo)) 38 | print(ROW.format('-' * 8, '-' * 30)) 39 | print(ROW.format(total, 'Total payments made')) 40 | 41 | def scrape_with_lxml(text): 42 | root = lxml.html.document_fromstring(text) 43 | total = 0 44 | for li in root.cssselect('li.to'): 45 | dollars = int(li.text_content().split()[0].lstrip('$')) 46 | memo = li.cssselect('i')[0].text_content() 47 | total += dollars 48 | print(ROW.format(dollars, memo)) 49 | print(ROW.format('-' * 8, '-' * 30)) 50 | print(ROW.format(total, 'Total payments made')) 51 | 52 | def main(): 53 | parser = argparse.ArgumentParser(description='Scrape our payments site.') 54 | parser.add_argument('url', help='the URL at which to begin') 55 | parser.add_argument('-l', action='store_true', help='scrape using lxml') 56 | parser.add_argument('-s', action='store_true', help='get with selenium') 57 | args = parser.parse_args() 58 | if args.s: 59 | text = download_page_with_selenium(args.url) 60 | else: 61 | text = download_page_with_requests(args.url) 62 | if args.l: 63 | scrape_with_lxml(text) 64 | else: 65 | scrape_with_soup(text) 66 | 67 | if __name__ == '__main__': 68 | main() 69 | -------------------------------------------------------------------------------- /source/chapter11/rscrape1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter11/rscrape1.py 4 | # Recursive scraper built using the Requests library. 5 | 6 | import argparse, requests 7 | from urllib.parse import urljoin, urlsplit 8 | from lxml import etree 9 | 10 | def GET(url): 11 | response = requests.get(url) 12 | if response.headers.get('Content-Type', '').split(';')[0] != 'text/html': 13 | return 14 | text = response.text 15 | try: 16 | html = etree.HTML(text) 17 | except Exception as e: 18 | print(' {}: {}'.format(e.__class__.__name__, e)) 19 | return 20 | links = html.findall('.//a[@href]') 21 | for link in links: 22 | yield GET, urljoin(url, link.attrib['href']) 23 | 24 | def scrape(start, url_filter): 25 | further_work = {start} 26 | already_seen = {start} 27 | while further_work: 28 | call_tuple = further_work.pop() 29 | function, url, *etc = call_tuple 30 | print(function.__name__, url, *etc) 31 | for call_tuple in function(url, *etc): 32 | if call_tuple in already_seen: 33 | continue 34 | already_seen.add(call_tuple) 35 | function, url, *etc = call_tuple 36 | if not url_filter(url): 37 | continue 38 | further_work.add(call_tuple) 39 | 40 | def main(GET): 41 | parser = argparse.ArgumentParser(description='Scrape a simple site.') 42 | parser.add_argument('url', help='the URL at which to begin') 43 | start_url = parser.parse_args().url 44 | starting_netloc = urlsplit(start_url).netloc 45 | url_filter = (lambda url: urlsplit(url).netloc == starting_netloc) 46 | scrape((GET, start_url), url_filter) 47 | 48 | if __name__ == '__main__': 49 | main(GET) 50 | -------------------------------------------------------------------------------- /source/chapter11/rscrape2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter11/rscrape2.py 4 | # Recursive scraper built using the Selenium Webdriver. 5 | 6 | from urllib.parse import urljoin 7 | from rscrape1 import main 8 | from selenium import webdriver 9 | 10 | class WebdriverVisitor: 11 | def __init__(self): 12 | self.browser = webdriver.Firefox() 13 | 14 | def GET(self, url): 15 | self.browser.get(url) 16 | yield from self.parse() 17 | if self.browser.find_elements_by_xpath('.//form'): 18 | yield self.submit_form, url 19 | 20 | def parse(self): 21 | # (Could also parse page.source with lxml yourself, as in scraper1.py) 22 | url = self.browser.current_url 23 | links = self.browser.find_elements_by_xpath('.//a[@href]') 24 | for link in links: 25 | yield self.GET, urljoin(url, link.get_attribute('href')) 26 | 27 | def submit_form(self, url): 28 | self.browser.get(url) 29 | self.browser.find_element_by_xpath('.//form').submit() 30 | yield from self.parse() 31 | 32 | if __name__ == '__main__': 33 | main(WebdriverVisitor().GET) 34 | -------------------------------------------------------------------------------- /source/chapter11/static/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | text-align: center; 3 | } 4 | body { 5 | display: inline-block; 6 | } 7 | .flash_message { 8 | position: relative; 9 | padding: 1em; 10 | color: white; 11 | background: green; 12 | } 13 | .flash_message a { 14 | position: absolute; 15 | top: 0px; 16 | right: 0px; 17 | margin-left: 0.5em; 18 | padding: 0.3em; 19 | color: white; 20 | text-decoration: none; 21 | font-family: sans-serif; 22 | font-weight: bold; 23 | } 24 | .complaint { 25 | display: block; 26 | padding-bottom: 1em; 27 | color: red; 28 | } 29 | ul { 30 | padding: 0px; 31 | text-align: left; 32 | } 33 | li { 34 | display: block; 35 | margin: 2px; 36 | padding: 2px 4px; 37 | } 38 | li.from { 39 | background-color: lightgreen; 40 | } 41 | li.to { 42 | background-color: pink; 43 | } 44 | form { 45 | display: block; 46 | float: left; 47 | clear: both; 48 | margin-left: auto; 49 | margin-right: auto; 50 | padding: 1em; 51 | box-shadow: 10px 10px 5px #aaa; 52 | border: 1px solid black; 53 | } 54 | label { 55 | display: block; 56 | text-align: center; 57 | } 58 | -------------------------------------------------------------------------------- /source/chapter11/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% block title %}{% endblock %} 4 | 5 | 6 | 7 |

{{ self.title() }}

8 | {% block body %}{% endblock %} 9 | 10 | 11 | -------------------------------------------------------------------------------- /source/chapter11/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Welcome, {{ username }}{% endblock %} 3 | {% block body %} 4 | {% for message in flash_messages %} 5 |
{{ message }}×
6 | {% endfor %} 7 |

Your Payments

8 |
    9 | {% for p in payments %} 10 | {% set prep = 'from' if (p.credit == username) else 'to' %} 11 | {% set acct = p.debit if (p.credit == username) else p.credit %} 12 |
  • ${{ p.dollars }} {{ prep }} {{ acct }} 13 | for: {{ p.memo }}
  • 14 | {% endfor %} 15 |
16 | Make payment | Log out 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /source/chapter11/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Please log in{% endblock %} 3 | {% block body %} 4 |
5 | 6 | 7 | 8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /source/chapter11/templates/pay.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Make a Payment{% endblock %} 3 | {% block body %} 4 |
5 | {% if complaint %}{{ complaint }}{% endif %} 6 | 7 | 8 | 9 | | Cancel 10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /source/chapter11/templates/pay2.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Make a Payment{% endblock %} 3 | {% block body %} 4 |
5 | {% if complaint %}{{ complaint }}{% endif %} 6 | 7 | 8 | 9 | 10 | | Cancel 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /source/chapter11/tinysite/further.html: -------------------------------------------------------------------------------- 1 |

2 | This paragraph element includes a literal inline link to 3 | page 5 and another link to 4 | page 6. 5 |

6 | -------------------------------------------------------------------------------- /source/chapter11/tinysite/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The Index 5 | 6 | 7 |

The Index

8 |

This page includes several different kinds of link.

9 |

Pages whose links appear in the HTML

10 |

11 | This paragraph element includes a literal inline link to 12 | page 1 and another link to 13 | page 2. 14 |

15 |

Pages that are only accessible through forms

16 |

17 | There is a search that this site can return: 18 |

19 | 20 | 21 |
22 |

23 |

Pages whose elements are loaded through JavaScript

24 |

25 | The following div element is populated through a callback. 26 |

27 |
28 | 40 |

41 | The first four words of this final paragraph are 42 | an anchor element that has no href attribute because it 43 | exists only to provide a name by which this paragraph can be 44 | referenced from a URL fragment — not to point anywhere else. 45 |

46 | 47 | -------------------------------------------------------------------------------- /source/chapter11/tinysite/page1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page 1 5 | 6 |

Page 1

7 |

Welcome to page 1.

8 | 9 | -------------------------------------------------------------------------------- /source/chapter11/tinysite/page2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page 2 5 | 6 |

Page 2

7 |

Welcome to page 2.

8 | 9 | -------------------------------------------------------------------------------- /source/chapter11/tinysite/page3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page 3 5 | 6 |

Page 3

7 |

Welcome to page 3.

8 | 9 | -------------------------------------------------------------------------------- /source/chapter11/tinysite/page4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page 4 5 | 6 |

Page 4

7 |

Welcome to page 4.

8 | 9 | -------------------------------------------------------------------------------- /source/chapter11/tinysite/page5.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page 5 5 | 6 |

Page 5

7 |

Welcome to page 5.

8 | 9 | -------------------------------------------------------------------------------- /source/chapter11/tinysite/page6.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page 6 5 | 6 |

Page 6

7 |

Welcome to page 6.

8 | 9 | -------------------------------------------------------------------------------- /source/chapter11/tinysite/search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Search Results 5 | 6 |

Search Results

7 |

Here are your search results:

8 | 12 | 13 | -------------------------------------------------------------------------------- /source/chapter12/attachment.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apress/foundations-of-python-network-programming-14/742320235e7a62854b7cbb4b21a4b1b3c6e07c08/source/chapter12/attachment.gz -------------------------------------------------------------------------------- /source/chapter12/attachment.txt: -------------------------------------------------------------------------------- 1 | This is a test 2 | -------------------------------------------------------------------------------- /source/chapter12/build_basic_email.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter12/build_basic_email.py 4 | 5 | import email.message, email.policy, email.utils, sys 6 | 7 | text = """Hello, 8 | This is a basic message from Chapter 12. 9 | - Anonymous""" 10 | 11 | def main(): 12 | message = email.message.EmailMessage(email.policy.SMTP) 13 | message['To'] = 'recipient@example.com' 14 | message['From'] = 'Test Sender ' 15 | message['Subject'] = 'Test Message, Chapter 12' 16 | message['Date'] = email.utils.formatdate(localtime=True) 17 | message['Message-ID'] = email.utils.make_msgid() 18 | message.set_content(text) 19 | sys.stdout.buffer.write(message.as_bytes()) 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /source/chapter12/build_mime_email.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter12/build_mime_email.py 4 | 5 | import argparse, email.message, email.policy, email.utils, mimetypes, sys 6 | 7 | plain = """Hello, 8 | This is a MIME message from Chapter 12. 9 | - Anonymous""" 10 | 11 | html = """

Hello,

12 |

This is a test message from Chapter 12.

13 |

- Anonymous

""" 14 | 15 | img = """

This is the smallest possible blue GIF:

16 | """ 17 | 18 | # Tiny example GIF from http://www.perlmonks.org/?node_id=7974 19 | blue_dot = (b'GIF89a1010\x900000\xff000,000010100\x02\x02\x0410;' 20 | .replace(b'0', b'\x00').replace(b'1', b'\x01')) 21 | 22 | def main(args): 23 | message = email.message.EmailMessage(email.policy.SMTP) 24 | message['To'] = 'Test Recipient ' 25 | message['From'] = 'Test Sender ' 26 | message['Subject'] = 'Foundations of Python Network Programming' 27 | message['Date'] = email.utils.formatdate(localtime=True) 28 | message['Message-ID'] = email.utils.make_msgid() 29 | 30 | if not args.i: 31 | message.set_content(html, subtype='html') 32 | message.add_alternative(plain) 33 | else: 34 | cid = email.utils.make_msgid() # RFC 2392: must be globally unique! 35 | message.set_content(html + img.format(cid.strip('<>')), subtype='html') 36 | message.add_related(blue_dot, 'image', 'gif', cid=cid, 37 | filename='blue-dot.gif') 38 | message.add_alternative(plain) 39 | 40 | for filename in args.filename: 41 | mime_type, encoding = mimetypes.guess_type(filename) 42 | if encoding or (mime_type is None): 43 | mime_type = 'application/octet-stream' 44 | main, sub = mime_type.split('/') 45 | if main == 'text': 46 | with open(filename, encoding='utf-8') as f: 47 | text = f.read() 48 | message.add_attachment(text, sub, filename=filename) 49 | else: 50 | with open(filename, 'rb') as f: 51 | data = f.read() 52 | message.add_attachment(data, main, sub, filename=filename) 53 | 54 | sys.stdout.buffer.write(message.as_bytes()) 55 | 56 | if __name__ == '__main__': 57 | parser = argparse.ArgumentParser(description='Build, print a MIME email') 58 | parser.add_argument('-i', action='store_true', help='Include GIF image') 59 | parser.add_argument('filename', nargs='*', help='Attachment filename') 60 | main(parser.parse_args()) 61 | -------------------------------------------------------------------------------- /source/chapter12/build_unicode_email.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter12/build_unicode_email.py 4 | 5 | import email.message, email.policy, sys 6 | 7 | text = """\ 8 | Hwær cwom mearg? Hwær cwom mago? 9 | Hwær cwom maþþumgyfa? 10 | Hwær cwom symbla gesetu? 11 | Hwær sindon seledreamas?""" 12 | 13 | def main(): 14 | message = email.message.EmailMessage(email.policy.SMTP) 15 | message['To'] = 'Böðvarr ' 16 | message['From'] = 'Eardstapa ' 17 | message['Subject'] = 'Four lines from The Wanderer' 18 | message['Date'] = email.utils.formatdate(localtime=True) 19 | message.set_content(text, cte='quoted-printable') 20 | sys.stdout.buffer.write(message.as_bytes()) 21 | 22 | if __name__ == '__main__': 23 | main() 24 | -------------------------------------------------------------------------------- /source/chapter12/display_email.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter12/display_email.py 4 | # 5 | # Someday this script will use message.iter_attachments() instead of 6 | # walking through them itself, but http://bugs.python.org/issue21079 7 | 8 | import argparse, email.policy, sys 9 | 10 | def main(binary_file): 11 | policy = email.policy.SMTP 12 | message = email.message_from_binary_file(binary_file, policy=policy) 13 | for header in ['From', 'To', 'Date', 'Subject']: 14 | print(header + ':', message.get(header, '(none)')) 15 | print() 16 | 17 | try: 18 | body = message.get_body(preferencelist=('plain', 'html')) 19 | except KeyError: 20 | print('') 21 | else: 22 | print(body.get_content()) 23 | 24 | for part in message.walk(): 25 | cd = part['Content-Disposition'] 26 | is_attachment = cd and cd.split(';')[0].lower() == 'attachment' 27 | if not is_attachment: 28 | continue 29 | content = part.get_content() 30 | print('* {} attachment named {!r}: {} object of length {}'.format( 31 | part.get_content_type(), part.get_filename(), 32 | type(content).__name__, len(content))) 33 | 34 | if __name__ == '__main__': 35 | parser = argparse.ArgumentParser(description='Parse and print an email') 36 | parser.add_argument('filename', nargs='?', help='File containing an email') 37 | args = parser.parse_args() 38 | if args.filename is None: 39 | main(sys.stdin.buffer) 40 | else: 41 | with open(args.filename, 'rb') as f: 42 | main(f) 43 | -------------------------------------------------------------------------------- /source/chapter12/display_structure.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter12/display_structure.py 4 | # 5 | # Someday this script will use part.is_attachment instead of making the 6 | # decision itself, but http://bugs.python.org/issue21079 7 | 8 | import argparse, email.policy, sys 9 | 10 | def walk(part, prefix=''): 11 | yield prefix, part 12 | for i, subpart in enumerate(part.iter_parts()): 13 | yield from walk(subpart, prefix + '.{}'.format(i)) 14 | 15 | def main(binary_file): 16 | policy = email.policy.SMTP 17 | message = email.message_from_binary_file(binary_file, policy=policy) 18 | for prefix, part in walk(message): 19 | line = '{} type={}'.format(prefix, part.get_content_type()) 20 | if not part.is_multipart(): 21 | content = part.get_content() 22 | line += ' {} len={}'.format(type(content).__name__, len(content)) 23 | cd = part['Content-Disposition'] 24 | is_attachment = cd and cd.split(';')[0].lower() == 'attachment' 25 | if is_attachment: 26 | line += ' attachment' 27 | filename = part.get_filename() 28 | if filename is not None: 29 | line += ' filename={!r}'.format(filename) 30 | print(line) 31 | 32 | if __name__ == '__main__': 33 | parser = argparse.ArgumentParser(description='Display MIME structure') 34 | parser.add_argument('filename', nargs='?', help='File containing an email') 35 | args = parser.parse_args() 36 | if args.filename is None: 37 | main(sys.stdin.buffer) 38 | else: 39 | with open(args.filename, 'rb') as f: 40 | main(f) 41 | -------------------------------------------------------------------------------- /source/chapter12/pre-python-3.4/README.rst: -------------------------------------------------------------------------------- 1 | 2 | ===================================== 3 | Chapter 12 scripts for Python <=3.3 4 | ===================================== 5 | 6 | This directory contains the e-mail message builder scripts from the 7 | second edition of Foundations of Python Network Programming, updated for 8 | Python 3 but otherwise unchanged. They should work with Python 3.3 or 9 | even earlier versions, in case you have not yet upgraded to Python 3.4 10 | and therefore cannot use the new API illustrated in the official 11 | ``chapter12`` directory. 12 | -------------------------------------------------------------------------------- /source/chapter12/pre-python-3.4/mime_decode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter12/pre-python-3.4/mime_decode.py 4 | 5 | import email, fileinput 6 | 7 | def save_parts(message, level=0, counter=1): 8 | l = "| " * level 9 | if message.is_multipart(): 10 | print(l + "Found multipart:") 11 | for item in message.get_payload(): 12 | counter = save_parts(item, level + 1, counter) 13 | else: 14 | filename = 'part{}.out'.format(counter) 15 | print('Part {}:'.format(counter), 16 | message.get('content-type', '-'), 17 | message.get('content-disposition', '-'), 18 | '=>', filename) 19 | with open(filename, 'wb') as f: 20 | f.write(message.get_payload(decode=1)) 21 | counter += 1 22 | return counter 23 | 24 | if __name__ == '__main__': 25 | message = email.message_from_string(''.join(fileinput.input())) 26 | save_parts(message) 27 | -------------------------------------------------------------------------------- /source/chapter12/pre-python-3.4/mime_gen_alt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter12/pre-python-3.4/mime_gen_alt.py 4 | 5 | from email.mime.base import MIMEBase 6 | from email.mime.multipart import MIMEMultipart 7 | from email.mime.text import MIMEText 8 | from email import utils, encoders 9 | 10 | def build_alternative(data, contenttype): 11 | maintype, subtype = contenttype.split('/') 12 | if maintype == 'text': 13 | part = MIMEText(data, _subtype=subtype) 14 | else: 15 | part = MIMEBase(maintype, subtype) 16 | part.set_payload(data) 17 | encoders.encode_base64(part) 18 | return part 19 | 20 | messagetext = """Hello, 21 | 22 | This is a *great* test message from Chapter 12. I hope you enjoy it! 23 | 24 | -- Anonymous""" 25 | messagehtml = """Hello,

26 | This is a great test message from Chapter 12. I hope you enjoy 27 | it!

28 | -- Anonymous""" 29 | 30 | 31 | msg = MIMEMultipart('alternative') 32 | msg['To'] = 'recipient@example.com' 33 | msg['From'] = 'Test Sender ' 34 | msg['Subject'] = 'Test Message, Chapter 12' 35 | msg['Date'] = utils.formatdate(localtime = 1) 36 | msg['Message-ID'] = utils.make_msgid() 37 | 38 | msg.attach(build_alternative(messagetext, 'text/plain')) 39 | msg.attach(build_alternative(messagehtml, 'text/html')) 40 | print(msg.as_string()) 41 | -------------------------------------------------------------------------------- /source/chapter12/pre-python-3.4/mime_gen_basic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter12/pre-python-3.4/mime_gen_basic.py 4 | 5 | from email.mime.base import MIMEBase 6 | from email.mime.multipart import MIMEMultipart 7 | from email.mime.text import MIMEText 8 | from email import utils, encoders 9 | import mimetypes, sys 10 | 11 | def build_attachment(filename): 12 | mimetype, mimeencoding = mimetypes.guess_type(filename) 13 | if mimeencoding or (mimetype is None): 14 | mimetype = 'application/octet-stream' 15 | maintype, subtype = mimetype.split('/') 16 | if maintype == 'text': 17 | with open(filename, 'r') as f: 18 | part = MIMEText(f.read(), _subtype=subtype) 19 | else: 20 | part = MIMEBase(maintype, subtype) 21 | with open(filename, 'rb') as f: 22 | part.set_payload(f.read()) 23 | encoders.encode_base64(part) 24 | part.add_header('Content-Disposition', 'attachment', 25 | filename = filename) 26 | return part 27 | 28 | message = """Hello, 29 | 30 | This is a test message from Chapter 12. I hope you enjoy it! 31 | 32 | -- Anonymous""" 33 | 34 | msg = MIMEMultipart() 35 | msg['To'] = 'recipient@example.com' 36 | msg['From'] = 'Test Sender ' 37 | msg['Subject'] = 'Test Message, Chapter 12' 38 | msg['Date'] = utils.formatdate(localtime = 1) 39 | msg['Message-ID'] = utils.make_msgid() 40 | 41 | body = MIMEText(message, _subtype='plain') 42 | msg.attach(body) 43 | for filename in sys.argv[1:]: 44 | msg.attach(build_attachment(filename)) 45 | print(msg.as_string()) 46 | -------------------------------------------------------------------------------- /source/chapter12/pre-python-3.4/mime_gen_both.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter12/pre-python-3.4/mime_gen_both.py 4 | 5 | from email.mime.text import MIMEText 6 | from email.mime.multipart import MIMEMultipart 7 | from email.mime.base import MIMEBase 8 | from email import utils, encoders 9 | import mimetypes, sys 10 | 11 | def build_part(data, contenttype): 12 | maintype, subtype = contenttype.split('/') 13 | if maintype == 'text': 14 | part = MIMEText(data, _subtype=subtype) 15 | else: 16 | part = MIMEBase(maintype, subtype) 17 | part.set_payload(data) 18 | encoders.encode_base64(part) 19 | return part 20 | 21 | def build_attachment(filename): 22 | mimetype, mimeencoding = mimetypes.guess_type(filename) 23 | if mimeencoding or (mimetype is None): 24 | mimetype = 'application/octet-stream' 25 | mode = 'r' if mimetype.startswith('text/') else 'rb' 26 | with open(filename, mode) as fd: 27 | part = build_part(fd.read(), mimetype) 28 | part.add_header('Content-Disposition', 'attachment', filename=filename) 29 | return part 30 | 31 | messagetext = """Hello, 32 | 33 | This is a *great* test message from Chapter 12. I hope you enjoy it! 34 | 35 | -- Anonymous""" 36 | messagehtml = """Hello,

37 | This is a great test message from Chapter 12. I hope you enjoy 38 | it!

39 | -- Anonymous""" 40 | 41 | msg = MIMEMultipart() 42 | msg['To'] = 'recipient@example.com' 43 | msg['From'] = 'Test Sender ' 44 | msg['Subject'] = 'Test Message, Chapter 12' 45 | msg['Date'] = utils.formatdate(localtime = 1) 46 | msg['Message-ID'] = utils.make_msgid() 47 | 48 | body = MIMEMultipart('alternative') 49 | body.attach(build_part(messagetext, 'text/plain')) 50 | body.attach(build_part(messagehtml, 'text/html')) 51 | msg.attach(body) 52 | 53 | for filename in sys.argv[1:]: 54 | msg.attach(build_attachment(filename)) 55 | print(msg.as_string()) 56 | -------------------------------------------------------------------------------- /source/chapter12/pre-python-3.4/mime_headers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter12/pre-python-3.4/mime_headers.py 4 | 5 | from email.mime.text import MIMEText 6 | from email.header import Header 7 | 8 | message_text = """Hello, 9 | 10 | This is a test message from Chapter 12. I hope you enjoy it! 11 | 12 | -- Anonymous""" 13 | 14 | message = MIMEText(message_text) 15 | message['To'] = 'recipient@example.com' 16 | header = Header() 17 | header.append('Michael Müller', 'iso-8859-1') # 'utf-8' is even more general 18 | header.append('') 19 | message['From'] = header 20 | message['Subject'] = 'Test Message, Chapter 12' 21 | 22 | print(message.as_string()) 23 | -------------------------------------------------------------------------------- /source/chapter12/pre-python-3.4/mime_parse_headers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter12/pre-python-3.4/mime_parse_headers.py 4 | 5 | import email, fileinput 6 | from email.header import decode_header 7 | 8 | message = email.message_from_string(''.join(fileinput.input())) 9 | for header, raw in list(message.items()): 10 | value = ''.join( 11 | data if isinstance(data, str) else data.decode(charset or 'ascii') 12 | for data, charset in decode_header(raw) 13 | ) 14 | print('{0}: {1}'.format(header, value)) 15 | -------------------------------------------------------------------------------- /source/chapter12/pre-python-3.4/mime_structure.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter12/pre-python-3.4/mime_structure.py 4 | 5 | import email, fileinput 6 | 7 | def print_message(message, level = 0): 8 | prefix = "| " * level 9 | prefix2 = prefix + "|" 10 | print(prefix + "+ Message Headers:") 11 | for header, value in message.items(): 12 | print(prefix2, header + ":", value) 13 | if message.is_multipart(): 14 | for item in message.get_payload(): 15 | print_message(item, level + 1) 16 | 17 | message = email.message_from_string(''.join(fileinput.input())) 18 | print_message(message) 19 | -------------------------------------------------------------------------------- /source/chapter12/pre-python-3.4/trad_gen_newhdrs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter12/pre-python-3.4/trad_gen_newhdrs.py 4 | # Traditional Message Generation with Date and Message-ID 5 | 6 | import email.utils 7 | from email.message import Message 8 | 9 | message = """Hello, 10 | 11 | This is a test message from Chapter 12. I hope you enjoy it! 12 | 13 | -- Anonymous""" 14 | 15 | msg = Message() 16 | msg['To'] = 'recipient@example.com' 17 | msg['From'] = 'Test Sender ' 18 | msg['Subject'] = 'Test Message, Chapter 12' 19 | msg['Date'] = email.utils.formatdate(localtime = 1) 20 | msg['Message-ID'] = email.utils.make_msgid() 21 | msg.set_payload(message) 22 | 23 | print(msg.as_string()) 24 | -------------------------------------------------------------------------------- /source/chapter12/pre-python-3.4/trad_gen_simple.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter12/pre-python-3.4/trad_gen_simple.py 4 | # Traditional Message Generation, Simple 5 | 6 | from email.message import Message 7 | text = """Hello, 8 | 9 | This is a test message from Chapter 12. I hope you enjoy it! 10 | 11 | -- Anonymous""" 12 | 13 | msg = Message() 14 | msg['To'] = 'recipient@example.com' 15 | msg['From'] = 'Test Sender ' 16 | msg['Subject'] = 'Test Message, Chapter 12' 17 | msg.set_payload(text) 18 | 19 | print(msg.as_string()) 20 | -------------------------------------------------------------------------------- /source/chapter12/pre-python-3.4/trad_parse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter12/pre-python-3.4/trad_parse.py 4 | # Traditional Message Parsing 5 | 6 | import email 7 | 8 | banner = '-' * 48 9 | popular_headers = {'From', 'To', 'Subject', 'Date'} 10 | with open('message.txt') as f: 11 | msg = email.message_from_file(f) 12 | headers = sorted(msg.keys()) 13 | 14 | print(banner) 15 | for header in headers: 16 | if header not in popular_headers: 17 | print(header + ':', msg[header]) 18 | print(banner) 19 | for header in headers: 20 | if header in popular_headers: 21 | print(header + ':', msg[header]) 22 | 23 | print(banner) 24 | if msg.is_multipart(): 25 | print("This program cannot handle MIME multipart messages.") 26 | else: 27 | print(msg.get_payload()) 28 | -------------------------------------------------------------------------------- /source/chapter13/README.md: -------------------------------------------------------------------------------- 1 | [Return to the Table of Contents](https://github.com/brandon-rhodes/fopnp#readme) 2 | 3 | # Chapter 13
SMTP 4 | 5 | This is a directory of program listings from Chapter 13 of the book: 6 | 7 |

8 |
Foundations of Python Network Programming
9 |
10 | Third Edition, October 2014
11 | by Brandon Rhodes and John Goerzen 12 |
13 |
14 | 15 | You can learn more about the book by visiting the 16 | [root of this GitHub source code repository](https://github.com/brandon-rhodes/fopnp#readme). 17 | 18 | These scripts were written for Python 3, but can also run successfully 19 | under Python 2. Simply use [3to2](https://pypi.python.org/pypi/3to2) to 20 | convert them to the older syntax. 21 | 22 | The scripts in this chapter are best exercised inside the network 23 | [Playground](../../playground#readme) where `mail.example.com` is 24 | already set up and configured to receive incoming email. Once the 25 | playground is running, ask for a prompt on the `h1` host and visit the 26 | `chapter13` directory: 27 | 28 | $ ./play.sh h1 29 | 30 | # cd py3/chapter13 31 | 32 | At the `h1` machine’s prompt you can then experiment with sending 33 | messages across the network. The `simple.py` script and the slightly 34 | more advanced `ehlo.py` do their work silently, while `debug.py` asks 35 | the Standard Library to show the communication that is going on at the 36 | socket level. 37 | 38 | ``` 39 | $ python3 simple.py mail.example.com sender@example.com brandon@example.com 40 | Message sent to 1 recipient 41 | ``` 42 | 43 | ``` 44 | $ python3 ehlo.py mail.example.com sender@example.com brandon@example.com 45 | Maximum message size is 10240000 46 | Message sent to 1 recipient 47 | ``` 48 | 49 | ``` 50 | $ python3 debug.py mail.example.com sender@example.com brandon@example.com 51 | send: 'ehlo [172.17.0.10]\r\n' 52 | reply: b'250-mail.example.com\r\n' 53 | reply: b'250-PIPELINING\r\n' 54 | reply: b'250-SIZE 10240000\r\n' 55 | reply: b'250-VRFY\r\n' 56 | reply: b'250-ETRN\r\n' 57 | reply: b'250-STARTTLS\r\n' 58 | reply: b'250-ENHANCEDSTATUSCODES\r\n' 59 | reply: b'250-8BITMIME\r\n' 60 | reply: b'250 DSN\r\n' 61 | reply: retcode (250); Msg: b'mail.example.com\nPIPELINING\nSIZE 10240000\nVRFY\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN' 62 | send: 'mail FROM: size=210\r\n' 63 | reply: b'250 2.1.0 Ok\r\n' 64 | reply: retcode (250); Msg: b'2.1.0 Ok' 65 | send: 'rcpt TO:\r\n' 66 | reply: b'250 2.1.5 Ok\r\n' 67 | reply: retcode (250); Msg: b'2.1.5 Ok' 68 | send: 'data\r\n' 69 | reply: b'354 End data with .\r\n' 70 | reply: retcode (354); Msg: b'End data with .' 71 | data: (354, b'End data with .') 72 | send: b'To: brandon@example.com\r\nFrom: sender@example.com\r\nSubject: Test Message from simple.py\r\n\r\nHello,\r\n\r\nThis is a test message sent to you from the debug.py program\r\nin Foundations of Python Network Programming.\r\n.\r\n' 73 | reply: b'250 2.0.0 Ok: queued as 98034261\r\n' 74 | reply: retcode (250); Msg: b'2.0.0 Ok: queued as 98034261' 75 | data: (250, b'2.0.0 Ok: queued as 98034261') 76 | Message sent to 1 recipient 77 | send: 'quit\r\n' 78 | reply: b'221 2.0.0 Bye\r\n' 79 | reply: retcode (221); Msg: b'2.0.0 Bye' 80 | ``` 81 | 82 | After connecting to the `mail` machine with the username `brandon` and 83 | the password `abc123` you can use the venerable `mail` command to see 84 | that three new test messages are in your mailbox, thanks to the three 85 | Python scripts that you just ran: 86 | 87 | # ssh brandon@mail.example.com 88 | brandon@mail.example.com's password: abc123 89 | 90 | You have new mail. 91 | 92 | $ mail 93 | "/var/mail/brandon": 6 messages 6 new 94 | >N 1 Administrator Tue Mar 25 17:14 9/345 Welcome to example.com! 95 | N 2 Administrator Mon Apr 21 12:08 11/431 Introduction to e-mail 96 | N 3 Test Sender Mon Apr 21 13:41 40/1135 Foundations of Python Net 97 | N 4 sender@example.com Wed Oct 22 20:34 14/477 Test Message from simple. 98 | N 5 sender@example.com Wed Oct 22 20:34 14/476 Test Message from simple. 99 | N 6 sender@example.com Wed Oct 22 20:35 14/475 Test Message from simple. 100 | ? q 101 | Held 6 messages in /var/mail/brandon 102 | 103 | The `login.py` and `tls.py` scripts are for the more advanced cases 104 | where an SMTP server requires authentication or TLS encryption — 105 | situations for which the Playground SMTP server is not yet configured. 106 | 107 | 108 | -------------------------------------------------------------------------------- /source/chapter13/debug.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter13/debug.py 4 | 5 | import sys, smtplib, socket 6 | 7 | message_template = """To: {} 8 | From: {} 9 | Subject: Test Message from simple.py 10 | 11 | Hello, 12 | 13 | This is a test message sent to you from the debug.py program 14 | in Foundations of Python Network Programming. 15 | """ 16 | 17 | def main(): 18 | if len(sys.argv) < 4: 19 | name = sys.argv[0] 20 | print("usage: {} server fromaddr toaddr [toaddr...]".format(name)) 21 | sys.exit(2) 22 | 23 | server, fromaddr, toaddrs = sys.argv[1], sys.argv[2], sys.argv[3:] 24 | message = message_template.format(', '.join(toaddrs), fromaddr) 25 | 26 | try: 27 | connection = smtplib.SMTP(server) 28 | connection.set_debuglevel(1) 29 | connection.sendmail(fromaddr, toaddrs, message) 30 | except (socket.gaierror, socket.error, socket.herror, 31 | smtplib.SMTPException) as e: 32 | print("Your message may not have been sent!") 33 | print(e) 34 | sys.exit(1) 35 | else: 36 | s = '' if len(toaddrs) == 1 else 's' 37 | print("Message sent to {} recipient{}".format(len(toaddrs), s)) 38 | connection.quit() 39 | 40 | if __name__ == '__main__': 41 | main() 42 | -------------------------------------------------------------------------------- /source/chapter13/ehlo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter13/ehlo.py 4 | 5 | import smtplib, socket, sys 6 | 7 | message_template = """To: {} 8 | From: {} 9 | Subject: Test Message from simple.py 10 | 11 | Hello, 12 | 13 | This is a test message sent to you from the ehlo.py program 14 | in Foundations of Python Network Programming. 15 | """ 16 | 17 | def main(): 18 | if len(sys.argv) < 4: 19 | name = sys.argv[0] 20 | print("usage: {} server fromaddr toaddr [toaddr...]".format(name)) 21 | sys.exit(2) 22 | 23 | server, fromaddr, toaddrs = sys.argv[1], sys.argv[2], sys.argv[3:] 24 | message = message_template.format(', '.join(toaddrs), fromaddr) 25 | 26 | try: 27 | connection = smtplib.SMTP(server) 28 | report_on_message_size(connection, fromaddr, toaddrs, message) 29 | except (socket.gaierror, socket.error, socket.herror, 30 | smtplib.SMTPException) as e: 31 | print("Your message may not have been sent!") 32 | print(e) 33 | sys.exit(1) 34 | else: 35 | s = '' if len(toaddrs) == 1 else 's' 36 | print("Message sent to {} recipient{}".format(len(toaddrs), s)) 37 | connection.quit() 38 | 39 | def report_on_message_size(connection, fromaddr, toaddrs, message): 40 | code = connection.ehlo()[0] 41 | uses_esmtp = (200 <= code <= 299) 42 | if not uses_esmtp: 43 | code = connection.helo()[0] 44 | if not (200 <= code <= 299): 45 | print("Remote server refused HELO; code:", code) 46 | sys.exit(1) 47 | 48 | if uses_esmtp and connection.has_extn('size'): 49 | print("Maximum message size is", connection.esmtp_features['size']) 50 | if len(message) > int(connection.esmtp_features['size']): 51 | print("Message too large; aborting.") 52 | sys.exit(1) 53 | 54 | connection.sendmail(fromaddr, toaddrs, message) 55 | 56 | if __name__ == '__main__': 57 | main() 58 | -------------------------------------------------------------------------------- /source/chapter13/login.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter13/login.py 4 | 5 | import sys, smtplib, socket 6 | from getpass import getpass 7 | 8 | message_template = """To: {} 9 | From: {} 10 | Subject: Test Message from simple.py 11 | 12 | Hello, 13 | 14 | This is a test message sent to you from the login.py program 15 | in Foundations of Python Network Programming. 16 | """ 17 | 18 | def main(): 19 | if len(sys.argv) < 4: 20 | name = sys.argv[0] 21 | print("Syntax: {} server fromaddr toaddr [toaddr...]".format(name)) 22 | sys.exit(2) 23 | 24 | server, fromaddr, toaddrs = sys.argv[1], sys.argv[2], sys.argv[3:] 25 | message = message_template.format(', '.join(toaddrs), fromaddr) 26 | 27 | username = input("Enter username: ") 28 | password = getpass("Enter password: ") 29 | 30 | try: 31 | connection = smtplib.SMTP(server) 32 | try: 33 | connection.login(username, password) 34 | except smtplib.SMTPException as e: 35 | print("Authentication failed:", e) 36 | sys.exit(1) 37 | connection.sendmail(fromaddr, toaddrs, message) 38 | except (socket.gaierror, socket.error, socket.herror, 39 | smtplib.SMTPException) as e: 40 | print("Your message may not have been sent!") 41 | print(e) 42 | sys.exit(1) 43 | else: 44 | s = '' if len(toaddrs) == 1 else 's' 45 | print("Message sent to {} recipient{}".format(len(toaddrs), s)) 46 | connection.quit() 47 | 48 | if __name__ == '__main__': 49 | main() 50 | -------------------------------------------------------------------------------- /source/chapter13/simple.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter13/simple.py 4 | 5 | import sys, smtplib 6 | 7 | message_template = """To: {} 8 | From: {} 9 | Subject: Test Message from simple.py 10 | 11 | Hello, 12 | 13 | This is a test message sent to you from the simple.py program 14 | in Foundations of Python Network Programming. 15 | """ 16 | 17 | def main(): 18 | if len(sys.argv) < 4: 19 | name = sys.argv[0] 20 | print("usage: {} server fromaddr toaddr [toaddr...]".format(name)) 21 | sys.exit(2) 22 | 23 | server, fromaddr, toaddrs = sys.argv[1], sys.argv[2], sys.argv[3:] 24 | message = message_template.format(', '.join(toaddrs), fromaddr) 25 | 26 | connection = smtplib.SMTP(server) 27 | connection.sendmail(fromaddr, toaddrs, message) 28 | connection.quit() 29 | 30 | s = '' if len(toaddrs) == 1 else 's' 31 | print("Message sent to {} recipient{}".format(len(toaddrs), s)) 32 | 33 | if __name__ == '__main__': 34 | main() 35 | -------------------------------------------------------------------------------- /source/chapter13/tls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter13/tls.py 4 | 5 | import sys, smtplib, socket, ssl 6 | 7 | message_template = """To: {} 8 | From: {} 9 | Subject: Test Message from simple.py 10 | 11 | Hello, 12 | 13 | This is a test message sent to you from the tls.py program 14 | in Foundations of Python Network Programming. 15 | """ 16 | 17 | def main(): 18 | if len(sys.argv) < 4: 19 | name = sys.argv[0] 20 | print("Syntax: {} server fromaddr toaddr [toaddr...]".format(name)) 21 | sys.exit(2) 22 | 23 | server, fromaddr, toaddrs = sys.argv[1], sys.argv[2], sys.argv[3:] 24 | message = message_template.format(', '.join(toaddrs), fromaddr) 25 | 26 | try: 27 | connection = smtplib.SMTP(server) 28 | send_message_securely(connection, fromaddr, toaddrs, message) 29 | except (socket.gaierror, socket.error, socket.herror, 30 | smtplib.SMTPException) as e: 31 | print("Your message may not have been sent!") 32 | print(e) 33 | sys.exit(1) 34 | else: 35 | s = '' if len(toaddrs) == 1 else 's' 36 | print("Message sent to {} recipient{}".format(len(toaddrs), s)) 37 | connection.quit() 38 | 39 | def send_message_securely(connection, fromaddr, toaddrs, message): 40 | code = connection.ehlo()[0] 41 | uses_esmtp = (200 <= code <= 299) 42 | if not uses_esmtp: 43 | code = connection.helo()[0] 44 | if not (200 <= code <= 299): 45 | print("Remove server refused HELO; code:", code) 46 | sys.exit(1) 47 | 48 | if uses_esmtp and connection.has_extn('starttls'): 49 | print("Negotiating TLS....") 50 | context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) 51 | context.set_default_verify_paths() 52 | context.verify_mode = ssl.CERT_REQUIRED 53 | connection.starttls(context=context) 54 | code = connection.ehlo()[0] 55 | if not (200 <= code <= 299): 56 | print("Couldn't EHLO after STARTTLS") 57 | sys.exit(5) 58 | print("Using TLS connection.") 59 | else: 60 | print("Server does not support TLS; using normal connection.") 61 | 62 | connection.sendmail(fromaddr, toaddrs, message) 63 | 64 | if __name__ == '__main__': 65 | main() 66 | -------------------------------------------------------------------------------- /source/chapter14/README.md: -------------------------------------------------------------------------------- 1 | [Return to the Table of Contents](https://github.com/brandon-rhodes/fopnp#readme) 2 | 3 | # Chapter 14
POP 4 | 5 | This is a directory of program listings from Chapter 14 of the book: 6 | 7 |
8 |
Foundations of Python Network Programming
9 |
10 | Third Edition, October 2014
11 | by Brandon Rhodes and John Goerzen 12 |
13 |
14 | 15 | You can learn more about the book by visiting the 16 | [root of this GitHub source code repository](https://github.com/brandon-rhodes/fopnp#readme). 17 | 18 | These scripts were written for Python 3, but can also run successfully 19 | under Python 2. Simply use [3to2](https://pypi.python.org/pypi/3to2) to 20 | convert them to the older syntax. 21 | 22 | You should probably never use the scripts in this chapter, as the POP 23 | protocol is unreliable, poorly designed and implemented on servers, and 24 | should be abandoned in favor of IMAP. See the chapter for details. 25 | 26 | The scripts in this chapter are best exercised inside the network 27 | [Playground](../../playground#readme) where `mail.example.com` is 28 | already set up and configured for POP. Once the playground is running, 29 | ask for a prompt on the `h1` host and visit this chapter’s directory: 30 | 31 | $ ./play.sh h1 32 | 33 | # cd py3/chapter14 34 | 35 | All of the scripts in this chapter are careful to use the `POP3_SSL` 36 | class and therefore guarantee the use of TLS to protect the user’s 37 | password and prevent other people in the same coffee shop from seeing 38 | the user’s email. The `popconn.py` script simply connects and reports 39 | the number of messages waiting: 40 | 41 | ``` 42 | $ python popconn.py mail.example.com brandon 43 | Password: abc123 44 | You have 6 messages totaling 3441 bytes 45 | ``` 46 | 47 | The `apopconn.py` script does exactly the same thing, but using a 48 | variant of the standard authentication methods. The `mailbox.py` script 49 | asks the server for a list of the messages that are waiting, and prints 50 | a brief summary about each one. 51 | 52 | ``` 53 | $ python mailbox.py mail.example.com brandon 54 | Password: abc123 55 | Message 1 has 354 bytes 56 | Message 2 has 442 bytes 57 | Message 3 has 1175 bytes 58 | Message 4 has 491 bytes 59 | Message 5 has 490 bytes 60 | Message 6 has 489 bytes 61 | ``` 62 | 63 | Finally, the `download_and_delete.py` script lets the user interactively 64 | view each message and decide whether to ask the server to delete it. 65 | -------------------------------------------------------------------------------- /source/chapter14/apopconn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter14/apopconn.py 4 | 5 | import getpass, poplib, sys 6 | 7 | def main(): 8 | if len(sys.argv) != 3: 9 | print('usage: %s hostname username' % sys.argv[0]) 10 | exit(2) 11 | 12 | hostname, username = sys.argv[1:] 13 | passwd = getpass.getpass() 14 | 15 | p = poplib.POP3_SSL(hostname) # or "POP3" if SSL is not supported 16 | try: 17 | p.apop(username, passwd) 18 | except poplib.error_proto as e: 19 | print("Login failed:", e) 20 | else: 21 | status = p.stat() 22 | print("You have %d messages totaling %d bytes" % status) 23 | finally: 24 | p.quit() 25 | 26 | if __name__ == '__main__': 27 | main() 28 | -------------------------------------------------------------------------------- /source/chapter14/download-and-delete.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter14/download-and-delete.py 4 | 5 | import email, getpass, poplib, sys 6 | 7 | def main(): 8 | if len(sys.argv) != 3: 9 | print('usage: %s hostname username' % sys.argv[0]) 10 | exit(2) 11 | 12 | hostname, username = sys.argv[1:] 13 | passwd = getpass.getpass() 14 | 15 | p = poplib.POP3_SSL(hostname) 16 | try: 17 | p.user(username) 18 | p.pass_(passwd) 19 | except poplib.error_proto as e: 20 | print("Login failed:", e) 21 | else: 22 | visit_all_listings(p) 23 | finally: 24 | p.quit() 25 | 26 | def visit_all_listings(p): 27 | response, listings, octets = p.list() 28 | for listing in listings: 29 | visit_listing(p, listing) 30 | 31 | def visit_listing(p, listing): 32 | number, size = listing.decode('ascii').split() 33 | print('Message', number, '(size is', size, 'bytes):') 34 | print() 35 | response, lines, octets = p.top(number, 0) 36 | document = '\n'.join( line.decode('ascii') for line in lines ) 37 | message = email.message_from_string(document) 38 | for header in 'From', 'To', 'Subject', 'Date': 39 | if header in message: 40 | print(header + ':', message[header]) 41 | print() 42 | print('Read this message [ny]?') 43 | answer = input() 44 | if answer.lower().startswith('y'): 45 | response, lines, octets = p.retr(number) 46 | document = '\n'.join( line.decode('ascii') for line in lines ) 47 | message = email.message_from_string(document) 48 | print('-' * 72) 49 | for part in message.walk(): 50 | if part.get_content_type() == 'text/plain': 51 | print(part.get_payload()) 52 | print('-' * 72) 53 | print() 54 | print('Delete this message [ny]?') 55 | answer = input() 56 | if answer.lower().startswith('y'): 57 | p.dele(number) 58 | print('Deleted.') 59 | 60 | if __name__ == '__main__': 61 | main() 62 | -------------------------------------------------------------------------------- /source/chapter14/mailbox.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter14/mailbox.py 4 | 5 | import getpass, poplib, sys 6 | 7 | def main(): 8 | if len(sys.argv) != 3: 9 | print('usage: %s hostname username' % sys.argv[0]) 10 | exit(2) 11 | 12 | hostname, username = sys.argv[1:] 13 | passwd = getpass.getpass() 14 | 15 | p = poplib.POP3_SSL(hostname) 16 | try: 17 | p.user(username) 18 | p.pass_(passwd) 19 | except poplib.error_proto as e: 20 | print("Login failed:", e) 21 | else: 22 | response, listings, octet_count = p.list() 23 | if not listings: 24 | print("No messages") 25 | for listing in listings: 26 | number, size = listing.decode('ascii').split() 27 | print("Message %s has %s bytes" % (number, size)) 28 | finally: 29 | p.quit() 30 | 31 | if __name__ == '__main__': 32 | main() 33 | -------------------------------------------------------------------------------- /source/chapter14/popconn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter14/popconn.py 4 | 5 | import getpass, poplib, sys 6 | 7 | def main(): 8 | if len(sys.argv) != 3: 9 | print('usage: %s hostname username' % sys.argv[0]) 10 | exit(2) 11 | 12 | hostname, username = sys.argv[1:] 13 | passwd = getpass.getpass() 14 | 15 | p = poplib.POP3_SSL(hostname) # or "POP3" if SSL is not supported 16 | try: 17 | p.user(username) 18 | p.pass_(passwd) 19 | except poplib.error_proto as e: 20 | print("Login failed:", e) 21 | else: 22 | status = p.stat() 23 | print("You have %d messages totaling %d bytes" % status) 24 | finally: 25 | p.quit() 26 | 27 | if __name__ == '__main__': 28 | main() 29 | -------------------------------------------------------------------------------- /source/chapter15/README.md: -------------------------------------------------------------------------------- 1 | [Return to the Table of Contents](https://github.com/brandon-rhodes/fopnp#readme) 2 | 3 | # Chapter 15
IMAP 4 | 5 | This is a directory of program listings from Chapter 15 of the book: 6 | 7 |
8 |
Foundations of Python Network Programming
9 |
10 | Third Edition, October 2014
11 | by Brandon Rhodes and John Goerzen 12 |
13 |
14 | 15 | You can learn more about the book by visiting the 16 | [root of this GitHub source code repository](https://github.com/brandon-rhodes/fopnp#readme). 17 | 18 | These scripts were written for Python 3, but can also run successfully 19 | under Python 2. Simply use [3to2](https://pypi.python.org/pypi/3to2) to 20 | convert them to the older syntax. 21 | 22 | The scripts in this chapter are best exercised inside the network 23 | [Playground](../../playground#readme) where `mail.example.com` is 24 | already set up and configured for POP. Once the playground is running, 25 | ask for a prompt on the `h1` host and visit this chapter’s directory: 26 | 27 | $ ./play.sh h1 28 | 29 | # cd py3/chapter14 30 | 31 | The scripts will all need the password of the `brandon` user, which is 32 | `abc123` and in the following examples will be piped in with the `echo` 33 | command. One lone script, `open_maplib.py`, uses the Standard Library 34 | `imaplib` module: 35 | 36 | ``` 37 | $ echo abc123 | python3 open_imaplib.py mail.example.com brandon 38 | Capabilities: ('IMAP4REV1', 'LITERAL+', 'SASL-IR', 'LOGIN-REFERRALS', 39 | 'ID', 'ENABLE', 'IDLE', 'AUTH=PLAIN') 40 | Listing mailboxes 41 | Status: 'OK' 42 | Data: 43 | b'(\\HasNoChildren) "/" INBOX' 44 | ``` 45 | 46 | All of the other scripts, for technical issues explained in the chapter, 47 | use the third party `imapclient` module by Menno Smits. It is both 48 | capable of negotiating additional capabilities with the server, and also 49 | parses the list of mailboxes on its own. 50 | 51 | ``` 52 | $ echo abc123 | python3 open_imap.py mail.example.com brandon 53 | Capabilities: ('IMAP4REV1', 'LITERAL+', 'SASL-IR', 'LOGIN-REFERRALS', 54 | 'ID', 'ENABLE', 'IDLE', 'SORT', 'SORT=DISPLAY', 'THREAD=REFERENCES', 55 | 'THREAD=REFS', 'THREAD=ORDEREDSUBJECT', 'MULTIAPPEND', 'URL-PARTIAL', 56 | 'CATENATE', 'UNSELECT', 'CHILDREN', 'NAMESPACE', 'UIDPLUS', 57 | 'LIST-EXTENDED', 'I18NLEVEL=1', 'CONDSTORE', 'QRESYNC', 'ESEARCH', 58 | 'ESORT', 'SEARCHRES', 'WITHIN', 'CONTEXT=SEARCH', 'LIST-STATUS', 59 | 'SPECIAL-USE', 'BINARY', 'MOVE') 60 | Listing mailboxes: 61 | \HasNoChildren / INBOX 62 | ``` 63 | 64 | Two further scripts show how to pull basic information about a folder, 65 | and how to provide a full summary of the messages inside. 66 | 67 | ``` 68 | $ echo abc123 | python3 folder_info.py mail.example.com brandon INBOX 69 | EXISTS: 3 70 | FLAGS: ('\\Answered', '\\Flagged', '\\Deleted', '\\Seen', '\\Draft') 71 | NOMODSEQ: [''] 72 | PERMANENTFLAGS: () 73 | READ-ONLY: [''] 74 | RECENT: 0 75 | UIDNEXT: 11 76 | UIDVALIDITY: 1414010141 77 | UNSEEN: ['1'] 78 | ``` 79 | 80 | ``` 81 | $ echo abc123 | python3 folder_summary.py mail.example.com brandon INBOX 82 | 1 Administrator 83 | We are happy that you have chosen to use example.com's indus ... 84 | 2 Administrator 85 | Administrator e-mails are sent as plain ASCII without MIME o ... 86 | 3 Test Sender 87 | Parts: multipart/alternative text/plain 88 | ``` 89 | 90 | The final script, `simple_client.py`, is quite elaborate (despite its 91 | name) and allows the user to interactively visit folders and the 92 | messages inside. 93 | -------------------------------------------------------------------------------- /source/chapter15/folder_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter15/folder_info.py 4 | # Opening an IMAP connection with IMAPClient and listing folder information. 5 | 6 | import getpass, sys 7 | from imapclient import IMAPClient 8 | 9 | def main(): 10 | if len(sys.argv) != 4: 11 | print('usage: %s hostname username foldername' % sys.argv[0]) 12 | sys.exit(2) 13 | 14 | hostname, username, foldername = sys.argv[1:] 15 | c = IMAPClient(hostname, ssl=True) 16 | try: 17 | c.login(username, getpass.getpass()) 18 | except c.Error as e: 19 | print('Could not log in:', e) 20 | else: 21 | select_dict = c.select_folder(foldername, readonly=True) 22 | for k, v in sorted(select_dict.items()): 23 | print('%s: %r' % (k, v)) 24 | finally: 25 | c.logout() 26 | 27 | if __name__ == '__main__': 28 | main() 29 | -------------------------------------------------------------------------------- /source/chapter15/folder_summary.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter15/folder_summary.py 4 | # Opening an IMAP connection with IMAPClient and retrieving mailbox messages. 5 | 6 | import email, getpass, sys 7 | from imapclient import IMAPClient 8 | 9 | def main(): 10 | if len(sys.argv) != 4: 11 | print('usage: %s hostname username foldername' % sys.argv[0]) 12 | sys.exit(2) 13 | 14 | hostname, username, foldername = sys.argv[1:] 15 | c = IMAPClient(hostname, ssl=True) 16 | try: 17 | c.login(username, getpass.getpass()) 18 | except c.Error as e: 19 | print('Could not log in:', e) 20 | else: 21 | print_summary(c, foldername) 22 | finally: 23 | c.logout() 24 | 25 | def print_summary(c, foldername): 26 | c.select_folder(foldername, readonly=True) 27 | msgdict = c.fetch('1:*', ['BODY.PEEK[]']) 28 | for message_id, message in list(msgdict.items()): 29 | e = email.message_from_string(message['BODY[]']) 30 | print(message_id, e['From']) 31 | payload = e.get_payload() 32 | if isinstance(payload, list): 33 | part_content_types = [ part.get_content_type() for part in payload ] 34 | print(' Parts:', ' '.join(part_content_types)) 35 | else: 36 | print(' ', ' '.join(payload[:60].split()), '...') 37 | 38 | if __name__ == '__main__': 39 | main() 40 | -------------------------------------------------------------------------------- /source/chapter15/open_imap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter15/open_imap.py 4 | # Opening an IMAP connection with the powerful IMAPClient 5 | 6 | import getpass, sys 7 | from imapclient import IMAPClient 8 | 9 | def main(): 10 | if len(sys.argv) != 3: 11 | print('usage: %s hostname username' % sys.argv[0]) 12 | sys.exit(2) 13 | 14 | hostname, username = sys.argv[1:] 15 | c = IMAPClient(hostname, ssl=True) 16 | try: 17 | c.login(username, getpass.getpass()) 18 | except c.Error as e: 19 | print('Could not log in:', e) 20 | else: 21 | print('Capabilities:', c.capabilities()) 22 | print('Listing mailboxes:') 23 | data = c.list_folders() 24 | for flags, delimiter, folder_name in data: 25 | print(' %-30s%s %s' % (' '.join(flags), delimiter, folder_name)) 26 | finally: 27 | c.logout() 28 | 29 | if __name__ == '__main__': 30 | main() 31 | -------------------------------------------------------------------------------- /source/chapter15/open_imaplib.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter15/open_imaplib.py 4 | # Opening an IMAP connection with the pitiful Python Standard Library 5 | 6 | import getpass, imaplib, sys 7 | 8 | def main(): 9 | if len(sys.argv) != 3: 10 | print('usage: %s hostname username' % sys.argv[0]) 11 | sys.exit(2) 12 | 13 | hostname, username = sys.argv[1:] 14 | m = imaplib.IMAP4_SSL(hostname) 15 | m.login(username, getpass.getpass()) 16 | try: 17 | print('Capabilities:', m.capabilities) 18 | print('Listing mailboxes ') 19 | status, data = m.list() 20 | print('Status:', repr(status)) 21 | print('Data:') 22 | for datum in data: 23 | print(repr(datum)) 24 | finally: 25 | m.logout() 26 | 27 | if __name__ == '__main__': 28 | main() 29 | -------------------------------------------------------------------------------- /source/chapter16/README.md: -------------------------------------------------------------------------------- 1 | [Return to the Table of Contents](https://github.com/brandon-rhodes/fopnp#readme) 2 | 3 | # Chapter 16
Telnet and SSH 4 | 5 | This is a directory of program listings from Chapter 16 of the book: 6 | 7 |
8 |
Foundations of Python Network Programming
9 |
10 | Third Edition, October 2014
11 | by Brandon Rhodes and John Goerzen 12 |
13 |
14 | 15 | You can learn more about the book by visiting the 16 | [root of this GitHub source code repository](https://github.com/brandon-rhodes/fopnp#readme). 17 | 18 | These scripts were written for Python 3, but can also run successfully 19 | under Python 2. Simply use [3to2](https://pypi.python.org/pypi/3to2) to 20 | convert them to the older syntax. 21 | 22 | Before diving into Telnet and SSH, the chapter introduces a small shell 23 | program `shell.py` that interprets nothing but whitespace as special on 24 | the command line, in an attempt to demonstrate to the skeptical reader 25 | that all of the characters they are used to treating as special are in 26 | fact quite ordinary down at the level of the operating system. 27 | 28 | Once the reader has been instructed as to the role of command lines, the 29 | features of terminals, and the pitfalls of special characters and 30 | quoting, two scripts are introduced which use the old insecure Telnet 31 | protocol. They can be exercised in the Playground against the 32 | `ftp.example.com` server which, just for fun, has a Telnet server 33 | running as well. 34 | 35 | ``` 36 | $ python telnet_login.py ftp.example.com brandon 37 | exec uptime 38 | 21:18:15 up 5:40, 1 user, load average: 0.00, 0.02, 0.05 39 | ``` 40 | 41 | ``` 42 | $ python telnet_codes.py ftp.example.com brandon 43 | Sending terminal type "mypython" 44 | ('Will not', 32) 45 | ('Will not', 35) 46 | ('Will not', 39) 47 | ('Do not', 3) 48 | ('Will not', 1) 49 | ('Will not', 31) 50 | ('Do not', 5) 51 | ('Will not', 33) 52 | ('Do not', 3) 53 | ('Do not', 1) 54 | exec echo My terminal type is $TERM 55 | My terminal type is mypython 56 | ``` 57 | 58 | When running the SSH scripts, you can target any of the hosts running in 59 | the Playground — they are all running SSH and should have both a `root` 60 | user and `brandon` user. For the examples below we will use the latter. 61 | To avoid having to edit the program listings to provide a password 62 | argument to `client.connect()`, ask SSH to install the `root` identity 63 | under the `brandon` account to which you wish to connect. Because 64 | Docker does not launch your initial shell on a host with everything set 65 | up as though you had logged in, this will only work if you `su` to the 66 | `root` user first: 67 | 68 | $ ./play.sh h1 69 | 70 | # su root 71 | 72 | # ssh-copy-id brandon@www.example.com 73 | 74 | By entering the `brandon` password `abc123` the ID should be copied 75 | successfully and a plain SSH command should then succeed without a 76 | password: 77 | 78 | # ssh brandon@www.example.com echo Success 79 | Success 80 | 81 | Once `ssh` itself is working without a password, the Python scripts 82 | should succeed as well. 83 | 84 | ``` 85 | $ python3 ssh_simple.py www.example.com brandon 86 | Welcome to Ubuntu 14.04.1 LTS (GNU/Linux 3.13.0-24-generic x86_64) 87 | 88 | * Documentation: https://help.ubuntu.com/ 89 | Last login: Wed Oct 22 21:38:21 2014 from modema 90 | echo Hello, world 91 | exit 92 | $ Hello, world 93 | $ 94 | ``` 95 | 96 | As you can see, trying to speak over a single communications channel to 97 | both a shell and the programs it invokes is something of a disaster. 98 | The book explains a better approach: 99 | 100 | ``` 101 | $ python3 ssh_commands.py www.example.com brandon 102 | b'Hello, world!\n' 103 | b'Linux\n' 104 | b' 21:38:33 up 6:00, 0 users, load average: 0.16, 0.05, 0.06\n' 105 | ``` 106 | 107 | The ability of SSH to support several channels even allows multiple 108 | Python threads to have remove commands running at the same time. 109 | 110 | ``` 111 | $ python3 ssh_threads.py www.example.com brandon 112 | One 113 | A 114 | B 115 | Two 116 | Three 117 | C 118 | ``` 119 | 120 | Finally, SSH includes a built-in file transfer protocol SFTP. 121 | 122 | ``` 123 | $ python3 sftp_get.py www.example.com brandon /etc/lsb-release 124 | Transfer of '/etc/lsb-release' is at 105/105 bytes (100.0%) 125 | Transfer of '/etc/lsb-release' is at 105/105 bytes (100.0%) 126 | ``` 127 | 128 | -------------------------------------------------------------------------------- /source/chapter16/sftp_get.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter16/sftp_get.py 4 | # Fetching files with SFTP 5 | 6 | import argparse, functools, paramiko 7 | 8 | class AllowAnythingPolicy(paramiko.MissingHostKeyPolicy): 9 | def missing_host_key(self, client, hostname, key): 10 | return 11 | 12 | def main(hostname, username, filenames): 13 | client = paramiko.SSHClient() 14 | client.set_missing_host_key_policy(AllowAnythingPolicy()) 15 | client.connect(hostname, username=username) # password='') 16 | 17 | def print_status(filename, bytes_so_far, bytes_total): 18 | percent = 100. * bytes_so_far / bytes_total 19 | print('Transfer of %r is at %d/%d bytes (%.1f%%)' % ( 20 | filename, bytes_so_far, bytes_total, percent)) 21 | 22 | sftp = client.open_sftp() 23 | for filename in filenames: 24 | if filename.endswith('.copy'): 25 | continue 26 | callback = functools.partial(print_status, filename) 27 | sftp.get(filename, filename + '.copy', callback=callback) 28 | client.close() 29 | 30 | if __name__ == '__main__': 31 | parser = argparse.ArgumentParser(description='Copy files over SSH') 32 | parser.add_argument('hostname', help='Remote machine name') 33 | parser.add_argument('username', help='Username on the remote machine') 34 | parser.add_argument('filename', nargs='+', help='Filenames to fetch') 35 | args = parser.parse_args() 36 | main(args.hostname, args.username, args.filename) 37 | -------------------------------------------------------------------------------- /source/chapter16/shell.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter16/shell.py 4 | # A simple shell, so you can try running commands at a prompt where no 5 | # characters are special (except that whitespace separates arguments). 6 | 7 | import subprocess 8 | 9 | def main(): 10 | while True: 11 | args = input('] ').strip().split() 12 | if not args: 13 | pass 14 | elif args == ['exit']: 15 | break 16 | elif args[0] == 'show': 17 | print("Arguments:", args[1:]) 18 | else: 19 | try: 20 | subprocess.call(args) 21 | except Exception as e: 22 | print(e) 23 | 24 | if __name__ == '__main__': 25 | main() 26 | -------------------------------------------------------------------------------- /source/chapter16/ssh_commands.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter16/ssh_commands.py 4 | # Running three separate commands, and reading three separate outputs 5 | 6 | import argparse, paramiko 7 | 8 | class AllowAnythingPolicy(paramiko.MissingHostKeyPolicy): 9 | def missing_host_key(self, client, hostname, key): 10 | return 11 | 12 | def main(hostname, username): 13 | client = paramiko.SSHClient() 14 | client.set_missing_host_key_policy(AllowAnythingPolicy()) 15 | client.connect(hostname, username=username) # password='') 16 | 17 | for command in 'echo "Hello, world!"', 'uname', 'uptime': 18 | stdin, stdout, stderr = client.exec_command(command) 19 | stdin.close() 20 | print(repr(stdout.read())) 21 | stdout.close() 22 | stderr.close() 23 | 24 | client.close() 25 | 26 | if __name__ == '__main__': 27 | parser = argparse.ArgumentParser(description='Connect over SSH') 28 | parser.add_argument('hostname', help='Remote machine name') 29 | parser.add_argument('username', help='Username on the remote machine') 30 | args = parser.parse_args() 31 | main(args.hostname, args.username) 32 | -------------------------------------------------------------------------------- /source/chapter16/ssh_simple.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter16/ssh_simple.py 4 | # Using SSH like Telnet: connecting and running two commands 5 | 6 | import argparse, paramiko, sys 7 | 8 | class AllowAnythingPolicy(paramiko.MissingHostKeyPolicy): 9 | def missing_host_key(self, client, hostname, key): 10 | return 11 | 12 | def main(hostname, username): 13 | client = paramiko.SSHClient() 14 | client.set_missing_host_key_policy(AllowAnythingPolicy()) 15 | client.connect(hostname, username=username) # password='') 16 | 17 | channel = client.invoke_shell() 18 | stdin = channel.makefile('wb') 19 | stdout = channel.makefile('rb') 20 | 21 | stdin.write(b'echo Hello, world\rexit\r') 22 | output = stdout.read() 23 | client.close() 24 | 25 | sys.stdout.buffer.write(output) 26 | 27 | if __name__ == '__main__': 28 | parser = argparse.ArgumentParser(description='Connect over SSH') 29 | parser.add_argument('hostname', help='Remote machine name') 30 | parser.add_argument('username', help='Username on the remote machine') 31 | args = parser.parse_args() 32 | main(args.hostname, args.username) 33 | -------------------------------------------------------------------------------- /source/chapter16/ssh_threads.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter16/ssh_threads.py 4 | # Running two remote commands simultaneously in different channels 5 | 6 | import argparse, paramiko, threading 7 | 8 | class AllowAnythingPolicy(paramiko.MissingHostKeyPolicy): 9 | def missing_host_key(self, client, hostname, key): 10 | return 11 | 12 | def main(hostname, username): 13 | client = paramiko.SSHClient() 14 | client.set_missing_host_key_policy(AllowAnythingPolicy()) 15 | client.connect(hostname, username=username) # password='') 16 | 17 | def read_until_EOF(fileobj): 18 | s = fileobj.readline() 19 | while s: 20 | print(s.strip()) 21 | s = fileobj.readline() 22 | 23 | ioe1 = client.exec_command('echo One;sleep 2;echo Two;sleep 1;echo Three') 24 | ioe2 = client.exec_command('echo A;sleep 1;echo B;sleep 2;echo C') 25 | thread1 = threading.Thread(target=read_until_EOF, args=(ioe1[1],)) 26 | thread2 = threading.Thread(target=read_until_EOF, args=(ioe2[1],)) 27 | thread1.start() 28 | thread2.start() 29 | thread1.join() 30 | thread2.join() 31 | 32 | client.close() 33 | 34 | if __name__ == '__main__': 35 | parser = argparse.ArgumentParser(description='Connect over SSH') 36 | parser.add_argument('hostname', help='Remote machine name') 37 | parser.add_argument('username', help='Username on the remote machine') 38 | args = parser.parse_args() 39 | main(args.hostname, args.username) 40 | -------------------------------------------------------------------------------- /source/chapter16/telnet_codes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter16/telnet_codes.py 4 | # How your code might look if you intercept Telnet options yourself 5 | 6 | import argparse, getpass 7 | from telnetlib import Telnet, IAC, DO, DONT, WILL, WONT, SB, SE, TTYPE 8 | 9 | def process_option(tsocket, command, option): 10 | if command == DO and option == TTYPE: 11 | tsocket.sendall(IAC + WILL + TTYPE) 12 | print('Sending terminal type "mypython"') 13 | tsocket.sendall(IAC + SB + TTYPE + b'\0' + b'mypython' + IAC + SE) 14 | elif command in (DO, DONT): 15 | print('Will not', ord(option)) 16 | tsocket.sendall(IAC + WONT + option) 17 | elif command in (WILL, WONT): 18 | print('Do not', ord(option)) 19 | tsocket.sendall(IAC + DONT + option) 20 | 21 | def main(hostname, username, password): 22 | t = Telnet(hostname) 23 | # t.set_debuglevel(1) # uncomment to get debug messages 24 | t.set_option_negotiation_callback(process_option) 25 | t.read_until(b'login:', 10) 26 | t.write(username.encode('utf-8') + b'\r') 27 | t.read_until(b'assword:', 10) # first letter might be 'p' or 'P' 28 | t.write(password.encode('utf-8') + b'\r') 29 | n, match, previous_text = t.expect([br'Login incorrect', br'\$'], 10) 30 | if n == 0: 31 | print("Username and password failed - giving up") 32 | else: 33 | t.write(b'exec echo My terminal type is $TERM\n') 34 | print(t.read_all().decode('ascii')) 35 | 36 | if __name__ == '__main__': 37 | parser = argparse.ArgumentParser(description='Use Telnet to log in') 38 | parser.add_argument('hostname', help='Remote host to telnet to') 39 | parser.add_argument('username', help='Remote username') 40 | args = parser.parse_args() 41 | password = getpass.getpass() 42 | main(args.hostname, args.username, password) 43 | -------------------------------------------------------------------------------- /source/chapter16/telnet_login.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter16/telnet_login.py 4 | # Connect to localhost, watch for a login prompt, and try logging in 5 | 6 | import argparse, getpass, telnetlib 7 | 8 | def main(hostname, username, password): 9 | t = telnetlib.Telnet(hostname) 10 | # t.set_debuglevel(1) # uncomment to get debug messages 11 | t.read_until(b'login:') 12 | t.write(username.encode('utf-8')) 13 | t.write(b'\r') 14 | t.read_until(b'assword:') # first letter might be 'p' or 'P' 15 | t.write(password.encode('utf-8')) 16 | t.write(b'\r') 17 | n, match, previous_text = t.expect([br'Login incorrect', br'\$'], 10) 18 | if n == 0: 19 | print('Username and password failed - giving up') 20 | else: 21 | t.write(b'exec uptime\r') 22 | print(t.read_all().decode('utf-8')) # read until socket closes 23 | 24 | if __name__ == '__main__': 25 | parser = argparse.ArgumentParser(description='Use Telnet to log in') 26 | parser.add_argument('hostname', help='Remote host to telnet to') 27 | parser.add_argument('username', help='Remote username') 28 | args = parser.parse_args() 29 | password = getpass.getpass('Password: ') 30 | main(args.hostname, args.username, password) 31 | -------------------------------------------------------------------------------- /source/chapter17/README.md: -------------------------------------------------------------------------------- 1 | [Return to the Table of Contents](https://github.com/brandon-rhodes/fopnp#readme) 2 | 3 | # Chapter 17
FTP 4 | 5 | This is a directory of program listings from Chapter 17 of the book: 6 | 7 |
8 |
Foundations of Python Network Programming
9 |
10 | Third Edition, October 2014
11 | by Brandon Rhodes and John Goerzen 12 |
13 |
14 | 15 | You can learn more about the book by visiting the 16 | [root of this GitHub source code repository](https://github.com/brandon-rhodes/fopnp#readme). 17 | 18 | These scripts were written for Python 3, but can also run successfully 19 | under Python 2. Simply use [3to2](https://pypi.python.org/pypi/3to2) to 20 | convert them to the older syntax. 21 | 22 | Most of the scripts in this chapter are for searching and downloading 23 | from FTP sites, and are hard-wired to run against well-known public 24 | servers that should give predictable results. 25 | 26 | ``` 27 | $ python3 connect.py 28 | Welcome: 220 ProFTPD Server 29 | Current working directory: / 30 | ``` 31 | 32 | ``` 33 | $ python3 nlst.py 34 | 13 entries: 35 | INDEX 36 | README 37 | ephem_4.28.tar.Z 38 | hawaii_scope 39 | incoming 40 | jupitor-moons.shar.Z 41 | lunar.c.Z 42 | lunisolar.shar.Z 43 | moon.shar.Z 44 | planetary 45 | sat-track.tar.Z 46 | stars.tar.Z 47 | xephem.tar.Z 48 | ``` 49 | 50 | ``` 51 | $ python3 dir.py 52 | 13 entries: 53 | -rw-r--r-- 1 48 25 341303 Oct 2 1992 ephem_4.28.tar.Z 54 | drwxr-xr-x 2 48 25 4096 Feb 11 1999 hawaii_scope 55 | drwxr-xr-x 2 48 utempter 4096 Feb 11 1999 incoming 56 | -rw-r--r-- 1 48 25 750 Feb 14 1994 INDEX 57 | -rw-r--r-- 1 48 25 5983 Oct 2 1992 jupitor-moons.shar.Z 58 | -rw-r--r-- 1 48 25 1751 Oct 2 1992 lunar.c.Z 59 | -rw-r--r-- 1 48 25 8078 Oct 2 1992 lunisolar.shar.Z 60 | -rw-r--r-- 1 48 25 64209 Oct 2 1992 moon.shar.Z 61 | drwxr-xr-x 2 48 25 4096 Jan 6 1993 planetary 62 | -rw-r--r-- 1 root bin 135 Feb 11 1999 README 63 | -rw-r--r-- 1 48 25 129969 Oct 2 1992 sat-track.tar.Z 64 | -rw-r--r-- 1 48 25 16504 Oct 2 1992 stars.tar.Z 65 | -rw-r--r-- 1 48 25 410650 Oct 2 1992 xephem.tar.Z 66 | ``` 67 | 68 | The simple download files are silent while doing their work, and you 69 | will have to list the current directory contents later to see that they 70 | had any effect. The advanced download scripts, by contrast, should 71 | constantly update the screen as you run them to report on progress. 72 | 73 | ``` 74 | $ python3 asciidl.py 75 | ``` 76 | 77 | ``` 78 | $ python3 binarydl.py 79 | ``` 80 | 81 | ``` 82 | $ python3 advbinarydl.py 83 | Received 1448 of 1259161 total bytes (0.1%) 84 | ... 85 | Received 1259161 of 1259161 total bytes (100.0%) 86 | ``` 87 | 88 | ``` 89 | $ python3 recursedl.py 90 | /pub/linux/kernel/Historic/old-versions 91 | /pub/linux/kernel/Historic/old-versions/impure 92 | /pub/linux/kernel/Historic/old-versions/old 93 | /pub/linux/kernel/Historic/old-versions/old/corrupt 94 | /pub/linux/kernel/Historic/old-versions/tytso 95 | ``` 96 | 97 | Finally, the two scripts for doing binary uploads are best run from 98 | inside of the [Playground](../../playground#readme) because it comes 99 | with a host named `ftp.example.com` that already has an FTP server 100 | installed. Try creating the `h1` host and moving into the `chapter17` 101 | directory: 102 | 103 | $ ./play h1 104 | 105 | # cd py3/chapter17 106 | 107 | The username `brandon` and the password `abc123` should let you upload 108 | binary files to the user’s home directory: 109 | 110 | ``` 111 | $ python3 binaryul.py ftp.example.com brandon /bin/true . 112 | Enter password for brandon on ftp.example.com: abc123 113 | ``` 114 | 115 | ``` 116 | $ python3 advbinaryul.py ftp.example.com brandon /bin/false . 117 | Enter password for brandon on ftp.example.com: abc123 118 | Sent 8192 of 27168 bytes (30.2%) 119 | Sent 16384 of 27168 bytes (60.3%) 120 | Sent 24576 of 27168 bytes (90.5%) 121 | Sent 27168 of 27168 bytes (100.0%) 122 | ``` 123 | 124 | A quick connection to the `ftp` host should then confirm that the files 125 | arrived successfully: 126 | 127 | # ssh brandon@ftp.example.com 128 | brandon@ftp.example.com's password: abc123 129 | 130 | Welcome to Ubuntu 14.04.1 LTS (GNU/Linux 3.13.0-24-generic x86_64) 131 | 132 | $ ls -l 133 | total 56 134 | -rw------- 1 brandon brandon 27168 Oct 23 01:47 false 135 | -rw------- 1 brandon brandon 27168 Oct 23 01:47 true 136 | -------------------------------------------------------------------------------- /source/chapter17/advbinarydl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter17/advbinarydl.py 4 | 5 | import os, sys 6 | from ftplib import FTP 7 | 8 | def main(): 9 | if os.path.exists('linux-1.0.tar.gz'): 10 | raise IOError('refusing to overwrite your linux-1.0.tar.gz file') 11 | 12 | ftp = FTP('ftp.kernel.org') 13 | ftp.login() 14 | ftp.cwd('/pub/linux/kernel/v1.0') 15 | ftp.voidcmd("TYPE I") 16 | 17 | socket, size = ftp.ntransfercmd("RETR linux-1.0.tar.gz") 18 | nbytes = 0 19 | 20 | f = open('linux-1.0.tar.gz', 'wb') 21 | 22 | while True: 23 | data = socket.recv(2048) 24 | if not data: 25 | break 26 | f.write(data) 27 | nbytes += len(data) 28 | print("\rReceived", nbytes, end=' ') 29 | if size: 30 | print("of %d total bytes (%.1f%%)" 31 | % (size, 100 * nbytes / float(size)), end=' ') 32 | else: 33 | print("bytes", end=' ') 34 | sys.stdout.flush() 35 | 36 | print() 37 | f.close() 38 | socket.close() 39 | ftp.voidresp() 40 | ftp.quit() 41 | 42 | if __name__ == '__main__': 43 | main() 44 | -------------------------------------------------------------------------------- /source/chapter17/advbinaryul.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter17/advbinaryul.py 4 | 5 | from ftplib import FTP 6 | import sys, getpass, os.path 7 | 8 | BLOCKSIZE = 8192 # chunk size to read and transmit: 8 kB 9 | 10 | def main(): 11 | if len(sys.argv) != 5: 12 | print("usage:", sys.argv[0], 13 | " ") 14 | exit(2) 15 | 16 | host, username, localfile, remotedir = sys.argv[1:] 17 | prompt = "Enter password for {} on {}: ".format(username, host) 18 | password = getpass.getpass(prompt) 19 | ftp = FTP(host) 20 | ftp.login(username, password) 21 | 22 | ftp.cwd(remotedir) 23 | ftp.voidcmd("TYPE I") 24 | datasock, esize = ftp.ntransfercmd('STOR %s' % os.path.basename(localfile)) 25 | size = os.stat(localfile)[6] 26 | nbytes = 0 27 | 28 | f = open(localfile, 'rb') 29 | while 1: 30 | data = f.read(BLOCKSIZE) 31 | if not data: 32 | break 33 | datasock.sendall(data) 34 | nbytes += len(data) 35 | print("\rSent", nbytes, "of", size, "bytes", 36 | "(%.1f%%)\r" % (100 * nbytes / float(size))) 37 | sys.stdout.flush() 38 | 39 | print() 40 | datasock.close() 41 | f.close() 42 | ftp.voidresp() 43 | ftp.quit() 44 | 45 | if __name__ == '__main__': 46 | main() 47 | -------------------------------------------------------------------------------- /source/chapter17/asciidl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter17/asciidl.py 4 | # Downloads README from remote and writes it to disk. 5 | 6 | import os 7 | from ftplib import FTP 8 | 9 | def main(): 10 | if os.path.exists('README'): 11 | raise IOError('refusing to overwrite your README file') 12 | 13 | ftp = FTP('ftp.kernel.org') 14 | ftp.login() 15 | ftp.cwd('/pub/linux/kernel') 16 | 17 | with open('README', 'w') as f: 18 | def writeline(data): 19 | f.write(data) 20 | f.write(os.linesep) 21 | 22 | ftp.retrlines('RETR README', writeline) 23 | 24 | ftp.quit() 25 | 26 | if __name__ == '__main__': 27 | main() 28 | -------------------------------------------------------------------------------- /source/chapter17/binarydl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter17/binarydl.py 4 | 5 | import os 6 | from ftplib import FTP 7 | 8 | def main(): 9 | if os.path.exists('patch8.gz'): 10 | raise IOError('refusing to overwrite your patch8.gz file') 11 | 12 | ftp = FTP('ftp.kernel.org') 13 | ftp.login() 14 | ftp.cwd('/pub/linux/kernel/v1.0') 15 | 16 | with open('patch8.gz', 'wb') as f: 17 | ftp.retrbinary('RETR patch8.gz', f.write) 18 | 19 | ftp.quit() 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /source/chapter17/binaryul.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter17/binaryul.py 4 | 5 | from ftplib import FTP 6 | import sys, getpass, os.path 7 | 8 | def main(): 9 | if len(sys.argv) != 5: 10 | print("usage:", sys.argv[0], 11 | " ") 12 | exit(2) 13 | 14 | host, username, localfile, remotedir = sys.argv[1:] 15 | prompt = "Enter password for {} on {}: ".format(username, host) 16 | password = getpass.getpass(prompt) 17 | 18 | ftp = FTP(host) 19 | ftp.login(username, password) 20 | ftp.cwd(remotedir) 21 | with open(localfile, 'rb') as f: 22 | ftp.storbinary('STOR %s' % os.path.basename(localfile), f) 23 | ftp.quit() 24 | 25 | if __name__ == '__main__': 26 | main() 27 | -------------------------------------------------------------------------------- /source/chapter17/connect.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter17/connect.py 4 | 5 | from ftplib import FTP 6 | 7 | def main(): 8 | ftp = FTP('ftp.ibiblio.org') 9 | print("Welcome:", ftp.getwelcome()) 10 | ftp.login() 11 | print("Current working directory:", ftp.pwd()) 12 | ftp.quit() 13 | 14 | if __name__ == '__main__': 15 | main() 16 | -------------------------------------------------------------------------------- /source/chapter17/dir.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter17/dir.py 4 | 5 | from ftplib import FTP 6 | 7 | def main(): 8 | ftp = FTP('ftp.ibiblio.org') 9 | ftp.login() 10 | ftp.cwd('/pub/academic/astronomy/') 11 | entries = [] 12 | ftp.dir(entries.append) 13 | ftp.quit() 14 | 15 | print(len(entries), "entries:") 16 | for entry in entries: 17 | print(entry) 18 | 19 | if __name__ == '__main__': 20 | main() 21 | -------------------------------------------------------------------------------- /source/chapter17/nlst.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter17/nlst.py 4 | 5 | from ftplib import FTP 6 | 7 | def main(): 8 | ftp = FTP('ftp.ibiblio.org') 9 | ftp.login() 10 | ftp.cwd('/pub/academic/astronomy/') 11 | entries = ftp.nlst() 12 | ftp.quit() 13 | 14 | print(len(entries), "entries:") 15 | for entry in sorted(entries): 16 | print(entry) 17 | 18 | if __name__ == '__main__': 19 | main() 20 | -------------------------------------------------------------------------------- /source/chapter17/recursedl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter17/recursedl.py 4 | 5 | from ftplib import FTP, error_perm 6 | 7 | def walk_dir(ftp, dirpath): 8 | original_dir = ftp.pwd() 9 | try: 10 | ftp.cwd(dirpath) 11 | except error_perm: 12 | return # ignore non-directores and ones we cannot enter 13 | print(dirpath) 14 | names = sorted(ftp.nlst()) 15 | for name in names: 16 | walk_dir(ftp, dirpath + '/' + name) 17 | ftp.cwd(original_dir) # return to cwd of our caller 18 | 19 | def main(): 20 | ftp = FTP('ftp.kernel.org') 21 | ftp.login() 22 | walk_dir(ftp, '/pub/linux/kernel/Historic/old-versions') 23 | ftp.quit() 24 | 25 | if __name__ == '__main__': 26 | main() 27 | -------------------------------------------------------------------------------- /source/chapter18/README.md: -------------------------------------------------------------------------------- 1 | [Return to the Table of Contents](https://github.com/brandon-rhodes/fopnp#readme) 2 | 3 | # Chapter 18
RPC 4 | 5 | This is a directory of program listings from Chapter 18 of the book: 6 | 7 |
8 |
Foundations of Python Network Programming
9 |
10 | Third Edition, October 2014
11 | by Brandon Rhodes and John Goerzen 12 |
13 |
14 | 15 | You can learn more about the book by visiting the 16 | [root of this GitHub source code repository](https://github.com/brandon-rhodes/fopnp#readme). 17 | 18 | These scripts were written for Python 3, but most of them will run under 19 | Python 2. Use [3to2](https://pypi.python.org/pypi/3to2) to convert them 20 | to the older syntax. The exception is that `xmlrpc_client.py` runs into 21 | trouble because it attempts to send Unicode strings in its RPC call, but 22 | the Standard Library `xmlrpclib` does not know how to encode them. 23 | 24 | The chapter explores three kinds of RPC: XML-RPC, JSON-RPC, and a 25 | Python-specific system called RPyC. The XML-RPC protocol is supported 26 | by the Standard Library, while the other two require third-party 27 | packages from the Python Package Index. 28 | 29 | In each case, the chapter provides both a small sample server and then a 30 | client that puts the server through its paces. As all of the examples 31 | are hard-coded to use `localhost`, they can all be safely run right on 32 | your machine. 33 | 34 | ## XML-RPC 35 | 36 | ``` 37 | $ python3 xmlrpc_server.py &>server.log & 38 | ``` 39 | 40 | ``` 41 | $ python3 xmlrpc_client.py 42 | xÿz 43 | 55 44 | [0.0, 8.0] 45 | [-1.0] 46 | [1, 2.0, 'three'] 47 | [1, 2.0, 'three'] 48 | {'data': {'age': 42, 'sex': 'M'}, 'name': 'Arthur'} 49 | Traceback (most recent call last): 50 | ... 51 | xmlrpc.client.Fault: :math domain error"> 52 | ``` 53 | 54 | ``` 55 | $ python3 xmlrpc_introspect.py 56 | Here are the functions supported by this server: 57 | addtogether(...) 58 | Add together everything in the list `things`. 59 | quadratic(...) 60 | Determine `x` values satisfying: `a` * x*x + `b` * x + c == 0 61 | remote_repr(...) 62 | Return the `repr()` rendering of the supplied `arg`. 63 | ``` 64 | 65 | ``` 66 | $ python3 xmlrpc_multicall.py 67 | abc 68 | [0.0, 8.0] 69 | [1, 2.0, 'three'] 70 | ``` 71 | 72 | ``` 73 | $ cat server.log 74 | Server ready 75 | 127.0.0.1 - - [25/Mar/2014 19:20:08] "POST /RPC2 HTTP/1.1" 200 - 76 | 127.0.0.1 - - [25/Mar/2014 19:20:08] "POST /RPC2 HTTP/1.1" 200 - 77 | ... 78 | 127.0.0.1 - - [25/Mar/2014 19:20:08] "POST /RPC2 HTTP/1.1" 200 - 79 | 127.0.0.1 - - [25/Mar/2014 19:20:08] "POST /RPC2 HTTP/1.1" 200 - 80 | ``` 81 | 82 | ## JSON-RPC 83 | 84 | ``` 85 | $ python3 jsonrpc_server.py &>server.log & 86 | ``` 87 | 88 | ``` 89 | $ python3 jsonrpc_client.py 90 | [[3, [1, 2, 3]], [None, 27], [2, {'Rigel': 0.12, 'Sirius': -1.46}]] 91 | ``` 92 | 93 | ``` 94 | $ cat server.log 95 | Starting server 96 | 127.0.0.1 - - [25/Mar/2014 19:20:08] "POST / HTTP/1.1" 200 - 97 | ``` 98 | 99 | ## RPyC 100 | 101 | ``` 102 | $ python3 rpyc_server.py &>server.log & 103 | ``` 104 | 105 | ``` 106 | $ python3 rpyc_client.py 107 | Noisy: 'Simple\n' 108 | Noisy: 'is\n' 109 | Noisy: 'better\n' 110 | Noisy: 'than\n' 111 | Noisy: 'complex.\n' 112 | The number of lines in the file was 5 113 | ``` 114 | 115 | ``` 116 | $ cat server.log 117 | Client has invoked exposed_line_counter() 118 | ``` 119 | 120 | Consult the chapter to learn about how RPyC is quite different from the 121 | other mechanisms. While the other two only support simple function 122 | invocation, RPyC is in fact a general two-way object publishing 123 | protocol. 124 | -------------------------------------------------------------------------------- /source/chapter18/jsonrpc_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter18/jsonrpc_client.py 4 | # JSON-RPC client needing "pip install jsonrpclib-pelix" 5 | 6 | from jsonrpclib import Server 7 | 8 | def main(): 9 | proxy = Server('http://localhost:7002') 10 | print(proxy.lengths((1,2,3), 27, {'Sirius': -1.46, 'Rigel': 0.12})) 11 | 12 | if __name__ == '__main__': 13 | main() 14 | -------------------------------------------------------------------------------- /source/chapter18/jsonrpc_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter18/jsonrpc_server.py 4 | # JSON-RPC server needing "pip install jsonrpclib-pelix" 5 | 6 | from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer 7 | 8 | def lengths(*args): 9 | """Measure the length of each input argument. 10 | 11 | Given N arguments, this function returns a list of N smaller 12 | lists of the form [len(arg), arg] that each state the length of 13 | an input argument and also echo back the argument itself. 14 | 15 | """ 16 | results = [] 17 | for arg in args: 18 | try: 19 | arglen = len(arg) 20 | except TypeError: 21 | arglen = None 22 | results.append((arglen, arg)) 23 | return results 24 | 25 | def main(): 26 | server = SimpleJSONRPCServer(('localhost', 7002)) 27 | server.register_function(lengths) 28 | print("Starting server") 29 | server.serve_forever() 30 | 31 | if __name__ == '__main__': 32 | main() 33 | -------------------------------------------------------------------------------- /source/chapter18/rpyc_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter18/rpyc_client.py 4 | # RPyC client 5 | 6 | import rpyc 7 | 8 | def main(): 9 | config = {'allow_public_attrs': True} 10 | proxy = rpyc.connect('localhost', 18861, config=config) 11 | fileobj = open('testfile.txt') 12 | linecount = proxy.root.line_counter(fileobj, noisy) 13 | print('The number of lines in the file was', linecount) 14 | 15 | def noisy(string): 16 | print('Noisy:', repr(string)) 17 | 18 | if __name__ == '__main__': 19 | main() 20 | -------------------------------------------------------------------------------- /source/chapter18/rpyc_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter18/rpyc_server.py 4 | # RPyC server 5 | 6 | import rpyc 7 | 8 | def main(): 9 | from rpyc.utils.server import ThreadedServer 10 | t = ThreadedServer(MyService, port = 18861) 11 | t.start() 12 | 13 | class MyService(rpyc.Service): 14 | def exposed_line_counter(self, fileobj, function): 15 | print('Client has invoked exposed_line_counter()') 16 | for linenum, line in enumerate(fileobj.readlines()): 17 | function(line) 18 | return linenum + 1 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /source/chapter18/testfile.txt: -------------------------------------------------------------------------------- 1 | Simple 2 | is 3 | better 4 | than 5 | complex. 6 | -------------------------------------------------------------------------------- /source/chapter18/xmlrpc_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Foundations of Python Network Programming, Third Edition 4 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter18/xmlrpc_client.py 5 | # XML-RPC client 6 | 7 | import xmlrpc.client 8 | 9 | def main(): 10 | proxy = xmlrpc.client.ServerProxy('http://127.0.0.1:7001') 11 | print(proxy.addtogether('x', 'ÿ', 'z')) 12 | print(proxy.addtogether(20, 30, 4, 1)) 13 | print(proxy.quadratic(2, -4, 0)) 14 | print(proxy.quadratic(1, 2, 1)) 15 | print(proxy.remote_repr((1, 2.0, 'three'))) 16 | print(proxy.remote_repr([1, 2.0, 'three'])) 17 | print(proxy.remote_repr({'name': 'Arthur', 18 | 'data': {'age': 42, 'sex': 'M'}})) 19 | print(proxy.quadratic(1, 0, 1)) 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /source/chapter18/xmlrpc_introspect.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter18/xmlrpc_introspect.py 4 | # XML-RPC client 5 | 6 | import xmlrpc.client 7 | 8 | def main(): 9 | proxy = xmlrpc.client.ServerProxy('http://127.0.0.1:7001') 10 | 11 | print('Here are the functions supported by this server:') 12 | for method_name in proxy.system.listMethods(): 13 | 14 | if method_name.startswith('system.'): 15 | continue 16 | 17 | signatures = proxy.system.methodSignature(method_name) 18 | if isinstance(signatures, list) and signatures: 19 | for signature in signatures: 20 | print('%s(%s)' % (method_name, signature)) 21 | else: 22 | print('%s(...)' % (method_name,)) 23 | 24 | method_help = proxy.system.methodHelp(method_name) 25 | if method_help: 26 | print(' ', method_help) 27 | 28 | if __name__ == '__main__': 29 | main() 30 | -------------------------------------------------------------------------------- /source/chapter18/xmlrpc_multicall.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter18/xmlrpc_multicall.py 4 | # XML-RPC client performing a multicall 5 | 6 | import xmlrpc.client 7 | 8 | def main(): 9 | proxy = xmlrpc.client.ServerProxy('http://127.0.0.1:7001') 10 | multicall = xmlrpc.client.MultiCall(proxy) 11 | multicall.addtogether('a', 'b', 'c') 12 | multicall.quadratic(2, -4, 0) 13 | multicall.remote_repr([1, 2.0, 'three']) 14 | for answer in multicall(): 15 | print(answer) 16 | 17 | if __name__ == '__main__': 18 | main() 19 | -------------------------------------------------------------------------------- /source/chapter18/xmlrpc_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Foundations of Python Network Programming, Third Edition 3 | # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter18/xmlrpc_server.py 4 | # XML-RPC server 5 | 6 | import operator, math 7 | from xmlrpc.server import SimpleXMLRPCServer 8 | from functools import reduce 9 | 10 | def main(): 11 | server = SimpleXMLRPCServer(('127.0.0.1', 7001)) 12 | server.register_introspection_functions() 13 | server.register_multicall_functions() 14 | server.register_function(addtogether) 15 | server.register_function(quadratic) 16 | server.register_function(remote_repr) 17 | print("Server ready") 18 | server.serve_forever() 19 | 20 | def addtogether(*things): 21 | """Add together everything in the list `things`.""" 22 | return reduce(operator.add, things) 23 | 24 | def quadratic(a, b, c): 25 | """Determine `x` values satisfying: `a` * x*x + `b` * x + c == 0""" 26 | b24ac = math.sqrt(b*b - 4.0*a*c) 27 | return list(set([ (-b-b24ac) / 2.0*a, 28 | (-b+b24ac) / 2.0*a ])) 29 | 30 | def remote_repr(arg): 31 | """Return the `repr()` rendering of the supplied `arg`.""" 32 | return arg 33 | 34 | if __name__ == '__main__': 35 | main() 36 | -------------------------------------------------------------------------------- /source/requirements.txt: -------------------------------------------------------------------------------- 1 | # Foundations of Python Network Programming 2 | # "requirements.txt" for Python 3 3 | # Makes it possible to run every script in the book 4 | 5 | beautifulsoup4 6 | cssselect 7 | dnspython3 8 | ghost.py==0.1b3 9 | httpbin 10 | imapclient 11 | jsonrpclib-pelix 12 | lxml 13 | paramiko 14 | pygeocoder 15 | python3-memcached 16 | pyzmq 17 | requests 18 | rpyc 19 | selenium 20 | webob 21 | werkzeug 22 | -------------------------------------------------------------------------------- /source/tools/monkeys/_bootlocale.py: -------------------------------------------------------------------------------- 1 | # If you scroll down you will see that this is not merely a cut and 2 | # paste of the usual contents of the _bootlocale.py file, but a way to 3 | # set up the state of Python so that the code examples in each chapter 4 | # README always run the same way. 5 | 6 | """A minimal subset of the locale module used at interpreter startup 7 | (imported by the _io module), in order to reduce startup time. 8 | 9 | Don't import directly from third-party code; use the `locale` module instead! 10 | """ 11 | import sys 12 | import _locale 13 | 14 | if sys.platform.startswith("win"): 15 | def getpreferredencoding(do_setlocale=True): 16 | return _locale._getdefaultlocale()[1] 17 | else: 18 | try: 19 | _locale.CODESET 20 | except AttributeError: 21 | def getpreferredencoding(do_setlocale=True): 22 | # This path for legacy systems needs the more complex 23 | # getdefaultlocale() function, import the full locale module. 24 | import locale 25 | return locale.getpreferredencoding(do_setlocale) 26 | else: 27 | def getpreferredencoding(do_setlocale=True): 28 | assert not do_setlocale 29 | result = _locale.nl_langinfo(_locale.CODESET) 30 | if not result and sys.platform == 'darwin': 31 | # nl_langinfo can return an empty string 32 | # when the setting has an invalid value. 33 | # Default to UTF-8 in that case because 34 | # UTF-8 is the default charset on OSX and 35 | # returning nothing will crash the 36 | # interpreter. 37 | result = 'UTF-8' 38 | return result 39 | 40 | # ADDED FOR FOUNDATIONS OF NETWORK PROGRAMMING session.txt REPRODUCABILITY: 41 | 42 | # Stabilize the random number generator so that various UUID functions 43 | # always return the same value. 44 | 45 | import random 46 | random.seed(0) 47 | del random 48 | 49 | # Set the current time so that messages built with the "email" library 50 | # have a stable "Date:" field. 51 | 52 | import time as time_module 53 | def time(): 54 | return 1395789608.667911 55 | time_module.time = time 56 | del time 57 | 58 | # Stabilize the current process PID so that "Message-ID:" fields stay 59 | # the same. 60 | 61 | import os 62 | def getpid(): 63 | return 15748 64 | os.getpid = getpid 65 | del getpid 66 | del os 67 | 68 | # Accept passwords from standard input instead of looking for the 69 | # controlling terminal. 70 | 71 | import getpass 72 | def fake_getpass(prompt='Password: '): 73 | print(prompt, 'abc123') 74 | return 'abc123' 75 | getpass.getpass = fake_getpass 76 | del getpass 77 | del fake_getpass 78 | 79 | # Replace print() with pretty-print when scripts print raw data 80 | # structures, so that dictionary keys come out in a stable order. 81 | 82 | from pprint import pprint 83 | builtin_print = print 84 | 85 | def print(*args, **kw): 86 | if len(args) == 1 and not isinstance(args[0], str): 87 | return pprint(args[0]) 88 | return builtin_print(*args, **kw) 89 | 90 | __builtins__['print'] = print 91 | del print 92 | -------------------------------------------------------------------------------- /source/tools/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Run sample shell commands found inside of a chapter README.md, and 4 | # rewrite the README.md with their real output. 5 | 6 | if [ -z "$1" ] 7 | then 8 | echo 'usage: run.sh ../chapter01/README.md' >&2 9 | exit 2 10 | fi 11 | 12 | original_directory=$(pwd) 13 | 14 | export LANG=C.UTF-8 15 | export PYTHONPATH=$(readlink -f $(dirname $0))/monkeys 16 | export PYTHONDONTWRITEBYTECODE=PLEASE 17 | 18 | for readme in "$@" 19 | do 20 | cd $(dirname $readme) 21 | while read line 22 | do 23 | echo "$line" 24 | if [ "$line" = '```' ] 25 | then 26 | read command_line 27 | echo "$command_line" 28 | command=${command_line:2} 29 | command=$(echo "$command" | sed 's/python\([23]\) /python\1 -u /') 30 | eval $command 31 | sleep 0.5 32 | read line 33 | while ! echo "$line" | grep -q '```' 34 | do read line 35 | done 36 | echo '```' 37 | fi 38 | done <$(basename $readme) >$(basename $readme).new 2>&1 39 | 40 | # Remove any temporary log file that might have been created. 41 | rm -f server.log 42 | 43 | # We "cat" instead of "mv" so the readme retains its original uid. 44 | cd $original_directory 45 | cat $readme.new > $readme 46 | rm -f $readme.new 47 | 48 | # Kill any server jobs that were started with "&" in the readme. 49 | if [ -n "$(jobs -p)" ] 50 | then kill $(jobs -p) 51 | fi 52 | done 53 | -------------------------------------------------------------------------------- /source/tools/two.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Try running each script through 3to2 to learn which ones can operate 4 | # as network programming examples under Python 2. 5 | 6 | set -e 7 | 8 | cd $(dirname ${BASH_SOURCE[0]}) 9 | cd .. 10 | for chapter in "$@" 11 | do 12 | two=$(echo $chapter | sed 's/chapter/two/') 13 | rm -rf $two 14 | cp -r $chapter $two 15 | sed 's/\$ python3 /\$ python2 /' $chapter/README.md > $two/README.md 16 | 3to2 -w $two/*.py 17 | tools/run.sh $two/README.md 18 | diff -u $chapter/README.md $two/README.md > $two/README.diff 19 | done 20 | 21 | # 3to2 -f imports -w two01/*.py 22 | --------------------------------------------------------------------------------