diff --git a/index.html b/index.html new file mode 100644 index 0000000..f54110f --- /dev/null +++ b/index.html @@ -0,0 +1,201 @@ + + + + + + ImageServer + + +
+ +
+
+ +
+
+ 加载更多 +
+ + + + diff --git a/network.py b/network.py new file mode 100644 index 0000000..fec2c76 --- /dev/null +++ b/network.py @@ -0,0 +1,251 @@ +import socket +import threading +import traceback +from http import HTTPStatus + +def parse_address(address): + try: + ip = address.split(':')[0] + port = int(address.split(':')[1]) + return ip, port + except: + print('Invalid address [{0}]').format(address) + print('exception information:') + print(traceback.format_exc()) + raise ValueError + +class BasicTCPServer(): + def __init__(self, address="127.0.0.1:12345", handler=None): + self.ip, self.port = parse_address(address) + + self.handler = handler if handler is not None else lambda clientsocket, addr:None + + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.socket.bind((self.ip, self.port)) + self.socket.listen(5) + self.socket.settimeout(0.5) + self.terminate = False + + def __del__(self): + self.stop() + + def set_handler(self, handler): + self.handler = handler + + def handle_message(self, clientsocket, addr): + self.handler(clientsocket, addr) + def loop(self): + try: + while not self.terminate: + try: + clientsocket,addr = self.socket.accept() + t = threading.Thread( + target=self.handle_message, + args=[clientsocket, addr], + name='Client[{0}]'.format(addr), + daemon=True + ) + t.start() + except socket.timeout: + pass + except socket.timeout: + pass + except (Exception, KeyboardInterrupt): + self.socket.close() + print('Bye~') + + def start(self, back=True): + if back: + t = threading.Thread(target=self.loop, name='SocketMainLoop', daemon=True) + t.start() + else: + self.loop() + + def stop(self): + self.terminate = True + self.socket.close() + +class HTTPBasicHeader(): + def __init__(self, words=None, content=None): + if content is None: + content = {} + self.words = words + self.content = content + + def encode(self): + contents = [' '.join([str(w) for w in self.words])] + contents += ['{0}: {1}'.format(name, value) for name, value in self.content.items()] + header_message = '\r\n'.join(contents) + header_message = header_message.encode('utf-8') + return header_message + + def decode(self, message): + if type(message) is bytes: + message = message.decode('utf-8') + contents = message.split('\r\n') + contents = [line.strip() for line in contents] + contents = [line for line in contents if line != ''] + header_line = contents[0] + contents = contents[1:] + words = header_line.split(' ') + valid_contents = {} + invalid_lines = [] + for line in contents: + delpos = line.find(':') + if delpos == -1: + invalid_lines.append(line) + else: + key = line[:delpos].strip() + value = line[delpos+1:].strip() + valid_contents[key] = value + if len(invalid_lines) > 0: + print('Warning: in-completed line found:') + print(invalid_lines) + return words, valid_contents + +class InvalidHTTPHeaderError(Exception): + def __init__(self, message=None): + pass + +class HTTPHeaderDictInterface(): + def __init__(self, content): + self.content = content + def __getitem__(self, index): + return self.content[index] + def __setitem__(self, index, value): + self.content[index] = value + def __contains__(self, index): + return index in self.content + def __iter__(self, index): + for key in self.content: + yield key + +class HTTPRequestHeader(HTTPHeaderDictInterface): + methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH'] + def __init__(self, method=None, url=None, version='HTTP/1.1', content=None): + if content is None: + content = {} + self.method = method + self.url = url + self.version = version + self.content = content + + def check_valid(self): + if self.method is None or self.url is None: + return False + elif self.method not in HTTPRequestHeader.methods: + return False + else: + return True + + def encode(self): + if not self.check_valid(): + raise InvalidHTTPHeaderError('Invalid header, method and url should at least be provided.') + words = [self.method, self.url, self.version] + content = self.content + message = HTTPBasicHeader(words=words, content=content).encode() + return message + def decode(self, message): + words, content = HTTPBasicHeader().decode(message) + if len(words) != 3: + raise InvalidHTTPHeaderError + self.method, self.url, self.version = words + self.content = content + +class HTTPResponseHeader(HTTPHeaderDictInterface): + def __init__(self, code=None, version='HTTP/1.1', content=None): + if content is None: + content = {} + self.code = code + self.version = version + self.content = content + + def check_valid(self): + if self.code is None: + return False + else: + return True + + def encode(self): + if not self.check_valid(): + raise InvalidHTTPHeaderError('Invalid header, code is required for a http header.') + words = [self.version, self.code, HTTPStatus(self.code).phrase] + content = self.content + message = HTTPBasicHeader(words=words, content=content).encode() + return message + def decode(self, message): + words, content = HTTPBasicHeader().decode(message) + if len(words) != 3: + raise InvalidHTTPHeaderError + self.version, self.code, _ = words + self.content = content + +class SingleHTTPConnection(): + def __init__(self, header, cached, connection): + self.header = header + self.connection = connection # connection is a basic socket connection. + self.cached = cached + self.length = 0 + if 'Content-Length' in self.header: + self.length = self.header['Content-Length'] # the remeaning legth of the connection. + + # To ensure all data is send, so we use sendall here. + def write(self, message): + self.connection.sendall(message) + + # this function will read fixed length from the socket. + def read_fixed_size(self, size): + if size <= 0: + return b'' + recvd = b'' + while len(recvd) < size: + this_message = self.connection.recv(size - len(recvd)) + if len(this_message) == 0: + break + recvd += this_message + return recvd + + def read(self, size=None): + if size is None: + self.length = 0 + return self.cached + self.read_fixed_size(self.length - self.cached) + if size > self.length: + size = self.length + message = b'' + message = self.cached[:size] + if len(message) < size: + message += self.read_fixed_size(size - len(message)) + self.length -= size + return message + +class HTTPBaseServer(BasicTCPServer): + def __init__(self, request_handler, bind_addr='127.0.0.1:80'): + if not callable(request_handler): + raise ValueError('You must provide an callable request handler.') + super(HTTPBaseServer, self).__init__(address=bind_addr) + self.request_handler = request_handler + + def handle_message(self, sock, addr): + # print('processing connection from ', addr) + # recieve until header ends. + delemeter = b'\r\n\r\n' + header_text = b'' + # print('New connection established from', addr) + while True: + # wait for the end of the + while header_text.find(delemeter) == -1: + this_mesage = sock.recv(8192) + if len(this_mesage) == 0: + # print('connection exited. addr:', addr) + return + header_text += this_mesage + delpos = header_text.find(delemeter) + content = header_text[delpos + len(delemeter):] + header_text = header_text[:delpos] + header = HTTPRequestHeader() + header.decode(header_text) + header_text = b'' + wraped_connection = SingleHTTPConnection(header, content, sock) + self.request_handler(wraped_connection) # the request handler can only read limited data, once finish, send, and return, we will move on. + # print('request handler finished.') diff --git a/server.py b/server.py new file mode 100644 index 0000000..a28b457 --- /dev/null +++ b/server.py @@ -0,0 +1,219 @@ +import os +import io +import json +import time + +import threading +import queue +from http import HTTPStatus +from urllib.parse import unquote +from PIL import Image + +from network import HTTPBaseServer, HTTPResponseHeader + +app_dir = os.path.split(os.path.realpath(__file__))[0] +index_path = os.path.join(app_dir, 'index.html') + +def loadfile(path): + with open(path, 'r', encoding='utf-8') as f: + return f.read() + +class HTTPImageServer(): + def __init__(self, bind_addr, imgroot='.'): + self.server = HTTPBaseServer(request_handler=self.handle, bind_addr=bind_addr) + self.imgroot = imgroot + self.img_extension = ['png', 'jpg', 'jpeg', 'tiff', 'webp', 'bmp'] + self.print_lock = threading.Lock() + self.logqueue = queue.Queue() + def start(self, back=True): + t = threading.Thread(target=self.logger, name='Logger thread', daemon=True) + t.start() + self.server.start(back=back) + + + def logger(self): + while True: + try: + msg = self.logqueue.get(timeout=1) + print(msg) + except queue.Empty: + pass + + @staticmethod + def parse_url(url): + location = url.split('?')[0] + params_str = url[len(location)+1:] + location = unquote(location) + params = {} + splits = params_str.split('&') + for split in splits: + split = unquote(split) + eq_pos = split.find('=') + if eq_pos == -1: + params[split] = None + continue + else: + key = split[:eq_pos] + value = split[eq_pos+1:] + params[key] = value + return location, params + + def log(self, msg): + self.logqueue.put(msg) + + def response(self, connection, header, content): + msg = '[{time}] {method}: {url} - {stat}'.format( + time = time.strftime("%H:%M:%S", time.localtime()), + method = connection.header.method, + url = connection.header.url, + stat = '{0}({1})'.format(header.code, HTTPStatus(header.code).phrase) + ) + self.log(msg) + + header['Content-Length'] = len(content) + connection.write(header.encode() + b'\r\n\r\n') + connection.write(content) + + def response_404(self, connection): + header = HTTPResponseHeader(404) + content = b'404 Not Found' + self.response(connection, header, content) + @staticmethod + def safe_path(path): + path = '/'.join(path.split('\\')) + path = path.split('/') + path = [p for p in path if p not in ['', '..', '.']] + path = '/'.join(path) + return path + + + def handle_index(self, params): + if 'path' not in params: + return HTTPResponseHeader(404), b'404 Not Found' + directory = params['path'] + while '\\' in directory: + directory = directory.replace('\\', '/') + directory = self.safe_path(directory) + disk_directory = os.path.join(self.imgroot, directory) + filenames = [] + try: + filenames = os.listdir(disk_directory) + filenames.sort() + except Exception: + pass + response = {"dirs": [], "imgs": []} + for filename in filenames: + full_path = os.path.join(disk_directory, filename) + request_path = '/{0}/{1}'.format(directory, filename) + request_path = '/' + request_path.strip('/\\') + if os.path.isdir(full_path): + response['dirs'].append(request_path) + else: + if filename.split('.')[-1] in self.img_extension: + response['imgs'].append(request_path) + response = json.dumps(response).encode('utf-8') + return HTTPResponseHeader(200), response + + def handle_image(self, params): + invalid_request = False + if 'path' not in params: + invalid_request = True + filepath = params['path'] + filepath = self.safe_path(filepath) + full_path = os.path.join(self.imgroot, filepath) + if filepath.split('.')[-1] not in self.img_extension: + invalid_request = True + elif not os.path.isfile(full_path): + invalid_request = True + + # parse height and width limit. + max_h, max_w = None, None + try: + if 'height' in params: + max_h = int(params['height']) + elif 'width' in params: + max_w = int(params['width']) + except Exception: + invalid_request = True + + if invalid_request: + return HTTPResponseHeader(404), b'404 Not Found' + + header = HTTPResponseHeader(200) + content = b'' + if max_h is not None or max_w is not None: + img = Image.open(full_path) + real_w, real_h = img.size + h_ratio = None + w_ratio = None + if max_h is not None: + h_ratio = max_h / real_h + h_ratio = h_ratio if h_ratio < 1 else 1 + if max_w is not None: + w_ratio = max_w / real_w + w_ratio = w_ratio if w_ratio < 1 else 1 + max_ratio = 0 + if h_ratio is None: + max_ratio = w_ratio + elif w_ratio is None: + max_ratio = h_ratio + else: + max_ratio = h_ratio if h_ratio < w_ratio else w_ratio + new_h, new_w = (real_h * max_ratio, real_w * max_ratio) + img = img.resize((int(new_w), int(new_h))) + img_stream = io.BytesIO() + img = img.save(img_stream, format='webp') + content = img_stream.getvalue() + else: + with open(full_path, 'rb') as f: + content = f.read() + return header, content + + + """ + request_type: + request index: http://domain.com/directory?path=relative/path/to/file + request image: http://domain.com/img?path=relative/path/to/file&height=100px&width=200px + """ + def handle(self, connection): + method = connection.header.method + if method != 'GET': + self.response_404(connection) + return + + url = connection.header.url + location, params = self.parse_url(url) + location = location.strip('/\\') + header, content = None, None + if location == 'directory': + header, content = self.handle_index(params) + elif location == 'img': + header, content = self.handle_image(params) + elif location in ['', 'index', 'index.html']: + header = HTTPResponseHeader(200) + content = loadfile(index_path).encode('utf-8') + else: + header = HTTPResponseHeader(404) + content = b'Please Do Not Try To Access Non-Image File!' + self.response(connection, header, content) + +if __name__ == '__main__': + import sys + args= sys.argv[1:] + + port = 80 + root = '.' + if len(args) > 0: + try: + port = int(args[0]) + except Exception: + print('Port {0} not understood, use 80 instead'.format(args[0]), file=sys.stderr) + if len(args) > 1: + root = args[1] + if not os.path.isdir(root): + print('Path {0} is not a valid path, use current directory instead.'.format(root), file=sys.stderr) + root = '.' + + print('Start HTTP server on port {0} and use web root as {1}'.format(port, root)) + server = HTTPImageServer(bind_addr='0.0.0.0:{0}'.format(port), imgroot=root) + server.start(back=False)