├── AUTHORS.md ├── image └── example.jpeg ├── .travis.yml ├── Dockerfile ├── SECURITY.md ├── CHANGELOG.md ├── LICENSE ├── .gitignore ├── README.md ├── .github └── workflows │ └── github-actions-test.yml └── simple_http_server.py /AUTHORS.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | freelamb 4 | -------------------------------------------------------------------------------- /image/example.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freelamb/simple_http_server/HEAD/image/example.jpeg -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.7" 5 | 6 | script: 7 | - date 8 | 9 | notifications: 10 | - email: true 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | 3 | RUN mkdir -p /opt/http_server /opt/data 4 | 5 | COPY simple_http_server.py /opt/http_server/ 6 | 7 | WORKDIR /opt/data 8 | 9 | ENV PORT=8000 10 | EXPOSE $PORT 11 | 12 | 13 | CMD python /opt/http_server/simple_http_server.py ${PORT} -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | | | 11 | 12 | 13 | ## Reporting a Vulnerability 14 | 15 | contact to freelamb@126.com 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [0.2.1] (2021-10-17) 3 | ### Features 4 | 5 | * fix windows signal SIGHUP error 6 | 7 | 8 | ## [0.2.0] (2021-05-04) 9 | ### Features 10 | 11 | * support python3.x 12 | 13 | 14 | ## [0.1.2] (2020-07-24) 15 | ### Features 16 | 17 | * fix Chinese garbled 18 | 19 | 20 | ## [0.1.0] (2018-04-10) 21 | 22 | ### Features 23 | 24 | * finish copy code 25 | 26 | 27 | ## [0.0.1] (2018-04-10) 28 | 29 | ### Features 30 | 31 | * init project 32 | * confirm requirement 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Yongbao Yang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | .idea 29 | .idea/ 30 | .idea/** 31 | 32 | .vscode 33 | .vscode/ 34 | .vscode/** 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | .hypothesis/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # SageMath parsed files 88 | *.sage.py 89 | 90 | # dotenv 91 | .env 92 | 93 | # virtualenv 94 | .venv 95 | venv/ 96 | ENV/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simple_http_server 2 | 3 | [![Build Status](https://travis-ci.org/freelamb/simple_http_server.svg?branch=master)](https://travis-ci.org/freelamb/simple_http_server) 4 | 5 | ## Features 6 | 7 | - ✔ simple 8 | - ✔ upload 9 | - ✔ download 10 | - ✔ support python2, python3 11 | ## Usage 12 | ```bash 13 | # get code 14 | $ git clone https://github.com/freelamb/simple_http_server.git 15 | 16 | # enter directory 17 | $ cd simple_http_server 18 | 19 | # run server 20 | $ python simple_http_server.py 8000 21 | 22 | # run as docker container 23 | # 1.build the image('.' below refer to the root path of this project) 24 | docker build -t freelamb/simple_http_server . 25 | # 2.run the container using the image built just now in docker 26 | docker run 27 | --name simple_http_server \ 28 | -p 8000:8000 \ 29 | -v /opt/data:/opt/data \ 30 | -d freelamb/simple_http_server:latest 31 | ``` 32 | 33 | ## Example 34 | 35 | ![](image/example.jpeg) 36 | 37 | ## Todo 38 | - [ ] support Multi-threaded 39 | - [ ] add docker images 40 | - [ ] add to pypi 41 | ## Contributing 42 | 43 | 1. Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug. 44 | 2. Fork [the repository](https://github.com/freelamb/simple_http_server)_ on GitHub to start making your changes to the **master** branch (or branch off of it). 45 | 3. Write a test which shows that the bug was fixed or that the feature works as expected. 46 | 4. Send a pull request and bug the maintainer until it gets merged and published. :) Make sure to add yourself to [AUTHORS_](AUTHORS.md). 47 | 48 | ## Changelog 49 | 50 | [Changelog](CHANGELOG.md) 51 | 52 | ## reference 53 | 54 | 55 | 56 | ## License 57 | 58 | [MIT](https://tldrlegal.com/license/mit-license) 59 | -------------------------------------------------------------------------------- /.github/workflows/github-actions-test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: build application 5 | 6 | on: 7 | push: 8 | branches: [ master, dev ] 9 | pull_request: 10 | branches: [ master, dev] 11 | 12 | jobs: 13 | linuxOS_build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 3.9 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: 3.9 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install flake8 pytest 25 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 26 | - name: Lint with flake8 27 | run: | 28 | # stop the build if there are Python syntax errors or undefined names 29 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 30 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 31 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 32 | MacOS_build: 33 | runs-on: macOS-latest 34 | steps: 35 | - uses: actions/checkout@v2 36 | - name: Set up Python 3.9 37 | uses: actions/setup-python@v2 38 | with: 39 | python-version: 3.9 40 | - name: Install dependencies 41 | run: | 42 | python -m pip install --upgrade pip 43 | pip install flake8 pytest 44 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 45 | - name: Lint with flake8 46 | run: | 47 | # stop the build if there are Python syntax errors or undefined names 48 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 49 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 50 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 51 | Windows_build: 52 | runs-on: windows-latest 53 | steps: 54 | - uses: actions/checkout@v2 55 | - name: Set up Python 3.9 56 | uses: actions/setup-python@v2 57 | with: 58 | python-version: 3.9 59 | - name: Install dependencies 60 | run: | 61 | python -m pip install --upgrade pip 62 | pip install flake8 pytest 63 | - name: Lint with flake8 64 | run: | 65 | # stop the build if there are Python syntax errors or undefined names 66 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 67 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 68 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 69 | 70 | -------------------------------------------------------------------------------- /simple_http_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | 4 | """Simple HTTP Server With Upload. 5 | This module builds on BaseHTTPServer by implementing the standard GET 6 | and HEAD requests in a fairly straightforward manner. 7 | """ 8 | 9 | __version__ = "0.3.2" 10 | __author__ = "yangyongbao@126.com" 11 | __all__ = ["SimpleHTTPRequestHandler"] 12 | 13 | import os 14 | import sys 15 | import argparse 16 | import posixpath 17 | try: 18 | from html import escape 19 | except ImportError: 20 | from cgi import escape 21 | import shutil 22 | import mimetypes 23 | import re 24 | import signal 25 | from io import StringIO, BytesIO 26 | 27 | if sys.version_info.major == 3: 28 | # Python3 29 | from urllib.parse import quote 30 | from urllib.parse import unquote 31 | from http.server import HTTPServer 32 | from http.server import BaseHTTPRequestHandler 33 | else: 34 | # Python2 35 | reload(sys) 36 | sys.setdefaultencoding('utf-8') 37 | from urllib import quote 38 | from urllib import unquote 39 | from BaseHTTPServer import HTTPServer 40 | from BaseHTTPServer import BaseHTTPRequestHandler 41 | 42 | 43 | class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): 44 | """Simple HTTP request handler with GET/HEAD/POST commands. 45 | This serves files from the current directory and any of its 46 | subdirectories. The MIME type for files is determined by 47 | calling the .guess_type() method. And can receive file uploaded 48 | by client. 49 | The GET/HEAD/POST requests are identical except that the HEAD 50 | request omits the actual contents of the file. 51 | """ 52 | 53 | server_version = "simple_http_server/" + __version__ 54 | 55 | def do_GET(self): 56 | """Serve a GET request.""" 57 | fd = self.send_head() 58 | if fd: 59 | shutil.copyfileobj(fd, self.wfile) 60 | fd.close() 61 | 62 | def do_HEAD(self): 63 | """Serve a HEAD request.""" 64 | fd = self.send_head() 65 | if fd: 66 | fd.close() 67 | 68 | def do_POST(self): 69 | """Serve a POST request.""" 70 | r, info = self.deal_post_data() 71 | print(r, info, "by: ", self.client_address) 72 | f = BytesIO() 73 | f.write(b'') 74 | f.write(b"\nUpload Result Page\n") 75 | f.write(b"\n

Upload Result Page

\n") 76 | f.write(b"
\n") 77 | if r: 78 | f.write(b"Success:") 79 | else: 80 | f.write(b"Failed:") 81 | f.write(info.encode('utf-8')) 82 | f.write(b"
back") 83 | f.write(b"
Powered By: freelamb, check new version at ") 84 | f.write(b"") 85 | f.write(b"here.\n\n") 86 | length = f.tell() 87 | f.seek(0) 88 | self.send_response(200) 89 | self.send_header("Content-type", "text/html;charset=utf-8") 90 | self.send_header("Content-Length", str(length)) 91 | self.end_headers() 92 | if f: 93 | shutil.copyfileobj(f, self.wfile) 94 | f.close() 95 | 96 | def deal_post_data(self): 97 | boundary = self.headers["Content-Type"].split("=")[1].encode('utf-8') 98 | remain_bytes = int(self.headers['content-length']) 99 | line = self.rfile.readline() 100 | remain_bytes -= len(line) 101 | if boundary not in line: 102 | return False, "Content NOT begin with boundary" 103 | line = self.rfile.readline() 104 | remain_bytes -= len(line) 105 | fn = re.findall(r'Content-Disposition.*name="file"; filename="(.*)"', line.decode('utf-8')) 106 | if not fn: 107 | return False, "Can't find out file name..." 108 | path = translate_path(self.path) 109 | fn = os.path.join(path, fn[0]) 110 | while os.path.exists(fn): 111 | fn += "_" 112 | line = self.rfile.readline() 113 | remain_bytes -= len(line) 114 | line = self.rfile.readline() 115 | remain_bytes -= len(line) 116 | try: 117 | out = open(fn, 'wb') 118 | except IOError: 119 | return False, "Can't create file to write, do you have permission to write?" 120 | 121 | pre_line = self.rfile.readline() 122 | remain_bytes -= len(pre_line) 123 | while remain_bytes > 0: 124 | line = self.rfile.readline() 125 | remain_bytes -= len(line) 126 | if boundary in line: 127 | pre_line = pre_line[0:-1] 128 | if pre_line.endswith(b'\r'): 129 | pre_line = pre_line[0:-1] 130 | out.write(pre_line) 131 | out.close() 132 | return True, "File '%s' upload success!" % fn 133 | else: 134 | out.write(pre_line) 135 | pre_line = line 136 | return False, "Unexpect Ends of data." 137 | 138 | def send_head(self): 139 | """Common code for GET and HEAD commands. 140 | This sends the response code and MIME headers. 141 | Return value is either a file object (which has to be copied 142 | to the output file by the caller unless the command was HEAD, 143 | and must be closed by the caller under all circumstances), or 144 | None, in which case the caller has nothing further to do. 145 | """ 146 | path = translate_path(self.path) 147 | if os.path.isdir(path): 148 | if not self.path.endswith('/'): 149 | # redirect browser - doing basically what apache does 150 | self.send_response(301) 151 | self.send_header("Location", self.path + "/") 152 | self.end_headers() 153 | return None 154 | for index in "index.html", "index.htm": 155 | index = os.path.join(path, index) 156 | if os.path.exists(index): 157 | path = index 158 | break 159 | else: 160 | return self.list_directory(path) 161 | content_type = self.guess_type(path) 162 | try: 163 | # Always read in binary mode. Opening files in text mode may cause 164 | # newline translations, making the actual size of the content 165 | # transmitted *less* than the content-length! 166 | f = open(path, 'rb') 167 | except IOError: 168 | self.send_error(404, "File not found") 169 | return None 170 | self.send_response(200) 171 | self.send_header("Content-type", content_type) 172 | fs = os.fstat(f.fileno()) 173 | self.send_header("Content-Length", str(fs[6])) 174 | self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) 175 | self.end_headers() 176 | return f 177 | 178 | def list_directory(self, path): 179 | """Helper to produce a directory listing (absent index.html). 180 | Return value is either a file object, or None (indicating an 181 | error). In either case, the headers are sent, making the 182 | interface the same as for send_head(). 183 | """ 184 | try: 185 | list_dir = os.listdir(path) 186 | except os.error: 187 | self.send_error(404, "No permission to list directory") 188 | return None 189 | list_dir.sort(key=lambda a: a.lower()) 190 | f = BytesIO() 191 | display_path = escape(unquote(self.path)) 192 | f.write(b'') 193 | f.write(b"\nDirectory listing for %s\n" % display_path.encode('utf-8')) 194 | f.write(b"\n

Directory listing for %s

\n" % display_path.encode('utf-8')) 195 | f.write(b"
\n") 196 | f.write(b"
") 197 | f.write(b"") 198 | f.write(b"
\n") 199 | f.write(b"
\n\n
\n\n\n") 212 | length = f.tell() 213 | f.seek(0) 214 | self.send_response(200) 215 | self.send_header("Content-type", "text/html;charset=utf-8") 216 | self.send_header("Content-Length", str(length)) 217 | self.end_headers() 218 | return f 219 | 220 | def guess_type(self, path): 221 | """Guess the type of a file. 222 | Argument is a PATH (a filename). 223 | Return value is a string of the form type/subtype, 224 | usable for a MIME Content-type header. 225 | The default implementation looks the file's extension 226 | up in the table self.extensions_map, using application/octet-stream 227 | as a default; however it would be permissible (if 228 | slow) to look inside the data to make a better guess. 229 | """ 230 | 231 | base, ext = posixpath.splitext(path) 232 | if ext in self.extensions_map: 233 | return self.extensions_map[ext] 234 | ext = ext.lower() 235 | if ext in self.extensions_map: 236 | return self.extensions_map[ext] 237 | else: 238 | return self.extensions_map[''] 239 | 240 | if not mimetypes.inited: 241 | mimetypes.init() # try to read system mime.types 242 | extensions_map = mimetypes.types_map.copy() 243 | extensions_map.update({ 244 | '': 'application/octet-stream', # Default 245 | '.py': 'text/plain', 246 | '.c': 'text/plain', 247 | '.h': 'text/plain', 248 | }) 249 | 250 | 251 | def translate_path(path): 252 | """Translate a /-separated PATH to the local filename syntax. 253 | Components that mean special things to the local file system 254 | (e.g. drive or directory names) are ignored. (XXX They should 255 | probably be diagnosed.) 256 | """ 257 | # abandon query parameters 258 | path = path.split('?', 1)[0] 259 | path = path.split('#', 1)[0] 260 | path = posixpath.normpath(unquote(path)) 261 | words = path.split('/') 262 | words = filter(None, words) 263 | path = os.getcwd() 264 | for word in words: 265 | drive, word = os.path.splitdrive(word) 266 | head, word = os.path.split(word) 267 | if word in (os.curdir, os.pardir): 268 | continue 269 | path = os.path.join(path, word) 270 | return path 271 | 272 | 273 | def signal_handler(signal, frame): 274 | print("You choose to stop me.") 275 | exit() 276 | 277 | def _argparse(): 278 | parser = argparse.ArgumentParser() 279 | parser.add_argument('--bind', '-b', metavar='ADDRESS', default='0.0.0.0', help='Specify alternate bind address [default: all interfaces]') 280 | parser.add_argument('--version', '-v', action='version', version=__version__) 281 | parser.add_argument('port', action='store', default=8000, type=int, nargs='?', help='Specify alternate port [default: 8000]') 282 | return parser.parse_args() 283 | 284 | def main(): 285 | args = _argparse() 286 | # print(args) 287 | server_address = (args.bind, args.port) 288 | signal.signal(signal.SIGINT, signal_handler) 289 | signal.signal(signal.SIGTERM, signal_handler) 290 | httpd = HTTPServer(server_address, SimpleHTTPRequestHandler) 291 | server = httpd.socket.getsockname() 292 | print("server_version: " + SimpleHTTPRequestHandler.server_version + ", python_version: " + SimpleHTTPRequestHandler.sys_version) 293 | print("sys encoding: " + sys.getdefaultencoding()) 294 | print("Serving http on: " + str(server[0]) + ", port: " + str(server[1]) + " ... (http://" + server[0] + ":" + str(server[1]) + "/)") 295 | httpd.serve_forever() 296 | 297 | if __name__ == '__main__': 298 | main() 299 | --------------------------------------------------------------------------------