├── proxy ├── requirements.txt ├── .idea │ ├── inspectionProfiles │ │ └── profiles_settings.xml │ ├── misc.xml │ ├── workspace.xml │ ├── modules.xml │ └── proxy.iml ├── Dockerfile └── monitor.py ├── test-client ├── requirements.txt ├── .idea │ ├── inspectionProfiles │ │ └── profiles_settings.xml │ ├── misc.xml │ ├── workspace.xml │ ├── modules.xml │ └── test-client.iml ├── Dockerfile └── tests │ ├── testutils.py │ ├── smoke_test.py │ ├── basic_test.py │ └── conftest.py ├── fastapi-server ├── requirements.txt ├── .idea │ ├── inspectionProfiles │ │ └── profiles_settings.xml │ ├── misc.xml │ ├── workspace.xml │ ├── modules.xml │ └── fastapi-server.iml ├── Dockerfile └── app │ └── main.py ├── nginx-server ├── html │ ├── n.gif │ ├── idc.gif │ ├── tbi.gif │ ├── about1.gif │ ├── action.gif │ ├── arrowr.gif │ ├── comdex.gif │ ├── enter.gif │ ├── msft.gif │ ├── one_sm.gif │ ├── prod.gif │ ├── search.gif │ ├── shop.gif │ ├── spacer.gif │ ├── sports.gif │ ├── vrule.gif │ ├── woofer.gif │ ├── 1ptrans.gif │ ├── Pacman1.gif │ ├── arrowbl.gif │ ├── arrowgr.gif │ ├── clinton.gif │ ├── comdex_6.gif │ ├── commish.gif │ ├── home_on.gif │ ├── nav_home.gif │ ├── netnow3.gif │ ├── rolodex.gif │ ├── spacer1.gif │ ├── spacer2.gif │ ├── support.gif │ ├── tagline.gif │ ├── appfoundry.gif │ ├── h_microsoft.gif │ ├── home_igloo.gif │ ├── ie_animated.gif │ ├── inbox_img.gif │ ├── msinternet.gif │ ├── solutions.gif │ ├── worldwide.gif │ ├── navigation_bar.gif │ ├── pointcast_small.gif │ ├── m.d.html │ └── microscape.html ├── .idea │ ├── inspectionProfiles │ │ └── profiles_settings.xml │ ├── misc.xml │ ├── workspace.xml │ ├── modules.xml │ └── nginx-server.iml └── default.conf ├── .idea ├── vcs.xml ├── inspectionProfiles │ └── profiles_settings.xml ├── .gitignore ├── webResources.xml ├── NetworksLab1Grading.iml └── modules.xml ├── LICENSE.md ├── docker-compose.yml ├── .gitignore └── README.md /proxy/requirements.txt: -------------------------------------------------------------------------------- 1 | pyzmq 2 | psutil -------------------------------------------------------------------------------- /test-client/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | httpx 3 | pyzmq -------------------------------------------------------------------------------- /fastapi-server/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn[standard] 3 | aiofiles -------------------------------------------------------------------------------- /nginx-server/html/n.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/n.gif -------------------------------------------------------------------------------- /nginx-server/html/idc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/idc.gif -------------------------------------------------------------------------------- /nginx-server/html/tbi.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/tbi.gif -------------------------------------------------------------------------------- /nginx-server/html/about1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/about1.gif -------------------------------------------------------------------------------- /nginx-server/html/action.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/action.gif -------------------------------------------------------------------------------- /nginx-server/html/arrowr.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/arrowr.gif -------------------------------------------------------------------------------- /nginx-server/html/comdex.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/comdex.gif -------------------------------------------------------------------------------- /nginx-server/html/enter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/enter.gif -------------------------------------------------------------------------------- /nginx-server/html/msft.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/msft.gif -------------------------------------------------------------------------------- /nginx-server/html/one_sm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/one_sm.gif -------------------------------------------------------------------------------- /nginx-server/html/prod.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/prod.gif -------------------------------------------------------------------------------- /nginx-server/html/search.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/search.gif -------------------------------------------------------------------------------- /nginx-server/html/shop.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/shop.gif -------------------------------------------------------------------------------- /nginx-server/html/spacer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/spacer.gif -------------------------------------------------------------------------------- /nginx-server/html/sports.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/sports.gif -------------------------------------------------------------------------------- /nginx-server/html/vrule.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/vrule.gif -------------------------------------------------------------------------------- /nginx-server/html/woofer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/woofer.gif -------------------------------------------------------------------------------- /nginx-server/html/1ptrans.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/1ptrans.gif -------------------------------------------------------------------------------- /nginx-server/html/Pacman1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/Pacman1.gif -------------------------------------------------------------------------------- /nginx-server/html/arrowbl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/arrowbl.gif -------------------------------------------------------------------------------- /nginx-server/html/arrowgr.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/arrowgr.gif -------------------------------------------------------------------------------- /nginx-server/html/clinton.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/clinton.gif -------------------------------------------------------------------------------- /nginx-server/html/comdex_6.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/comdex_6.gif -------------------------------------------------------------------------------- /nginx-server/html/commish.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/commish.gif -------------------------------------------------------------------------------- /nginx-server/html/home_on.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/home_on.gif -------------------------------------------------------------------------------- /nginx-server/html/nav_home.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/nav_home.gif -------------------------------------------------------------------------------- /nginx-server/html/netnow3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/netnow3.gif -------------------------------------------------------------------------------- /nginx-server/html/rolodex.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/rolodex.gif -------------------------------------------------------------------------------- /nginx-server/html/spacer1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/spacer1.gif -------------------------------------------------------------------------------- /nginx-server/html/spacer2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/spacer2.gif -------------------------------------------------------------------------------- /nginx-server/html/support.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/support.gif -------------------------------------------------------------------------------- /nginx-server/html/tagline.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/tagline.gif -------------------------------------------------------------------------------- /nginx-server/html/appfoundry.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/appfoundry.gif -------------------------------------------------------------------------------- /nginx-server/html/h_microsoft.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/h_microsoft.gif -------------------------------------------------------------------------------- /nginx-server/html/home_igloo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/home_igloo.gif -------------------------------------------------------------------------------- /nginx-server/html/ie_animated.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/ie_animated.gif -------------------------------------------------------------------------------- /nginx-server/html/inbox_img.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/inbox_img.gif -------------------------------------------------------------------------------- /nginx-server/html/msinternet.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/msinternet.gif -------------------------------------------------------------------------------- /nginx-server/html/solutions.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/solutions.gif -------------------------------------------------------------------------------- /nginx-server/html/worldwide.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/worldwide.gif -------------------------------------------------------------------------------- /nginx-server/html/navigation_bar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/navigation_bar.gif -------------------------------------------------------------------------------- /nginx-server/html/pointcast_small.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenSUTD/50.012_2023_lab1_tests/main/nginx-server/html/pointcast_small.gif -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /proxy/.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /proxy/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /nginx-server/.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /nginx-server/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test-client/.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /test-client/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /fastapi-server/.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /fastapi-server/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | /aws.xml 10 | /misc.xml 11 | -------------------------------------------------------------------------------- /proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11 2 | RUN apt update -y && apt install -y moreutils lsof psmisc net-tools 3 | COPY requirements.txt ./requirements.txt 4 | RUN pip install -r requirements.txt 5 | COPY monitor.py /monitor.py 6 | ENTRYPOINT ["python", "-u", "monitor.py"] -------------------------------------------------------------------------------- /proxy/.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /test-client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11 2 | WORKDIR /app 3 | COPY ./requirements.txt requirements.txt 4 | RUN pip install -r requirements.txt 5 | COPY tests /app/tests 6 | WORKDIR /app/tests 7 | ENV PYTHONUNBUFFERED 1 8 | CMD "pytest" "${PYTEST_TESTS:=.}" | tee /var/log/pytest/result.log 9 | 10 | -------------------------------------------------------------------------------- /proxy/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test-client/.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /fastapi-server/.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /fastapi-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11 2 | 3 | WORKDIR /code 4 | 5 | COPY ./requirements.txt /code/requirements.txt 6 | 7 | RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt 8 | 9 | COPY ./app /code/app 10 | 11 | CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] -------------------------------------------------------------------------------- /nginx-server/.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /nginx-server/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test-client/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /fastapi-server/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /nginx-server/.idea/nginx-server.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /proxy/.idea/proxy.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/webResources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test-client/.idea/test-client.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /fastapi-server/.idea/fastapi-server.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/NetworksLab1Grading.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /nginx-server/default.conf: -------------------------------------------------------------------------------- 1 | 2 | log_format log_req_resp '{"time": $msec, "remote": "$remote_addr:$remote_port", "request_line": "$request", "status": $status, "response_hash":"$resp_body_hash"}'; 3 | 4 | server { 5 | access_log /var/log/nginx/access.log log_req_resp; 6 | lua_need_request_body on; 7 | set $resp_body ""; 8 | set $resp_body_hash ""; 9 | body_filter_by_lua ' 10 | local resp_body = string.sub(ngx.arg[1], 1, 1000) 11 | ngx.ctx.buffered = (ngx.ctx.buffered or "") .. resp_body 12 | if ngx.arg[2] then 13 | ngx.var.resp_body = ngx.ctx.buffered 14 | end 15 | local resty_md5 = require "resty.md5" 16 | local md5 = resty_md5:new() 17 | md5:update(resp_body) 18 | local digest = md5:final() 19 | local str = require "resty.string" 20 | ngx.var.resp_body_hash = str.to_hex(digest) 21 | '; 22 | location / { 23 | index microscape.html; 24 | } 25 | } -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 OpenSUTD 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. -------------------------------------------------------------------------------- /test-client/tests/testutils.py: -------------------------------------------------------------------------------- 1 | import json 2 | from math import floor 3 | from typing import List, Dict, Any 4 | 5 | 6 | def get_nginx_log_entries_after_time(time: float | int) -> List[Dict[str, Any]]: 7 | start_time = floor(time) 8 | with open("/var/log/nginx/access.log", "r") as f: 9 | while True: 10 | line = f.readline().strip() 11 | if not line: 12 | assert False, "Did not seem to find a future log entry in nginx after firing the request" 13 | log_object = json.loads(line) 14 | if log_object["time"] >= start_time: 15 | log_entries = [log_object] + [json.loads(s) for s in f.readlines()] 16 | return log_entries 17 | 18 | 19 | def get_fastapi_log_entries_after_time(time: float | int) -> List[Dict[str, Any]]: 20 | start_time = floor(time) 21 | with open("/var/log/fastapi/access.log", "r") as f: 22 | while True: 23 | line = f.readline().strip() 24 | if not line: 25 | assert False, "Did not seem to find a future log entry in nginx after firing the request" 26 | log_object = json.loads(line) 27 | if log_object["time"] >= start_time: 28 | log_entries = [log_object] + [json.loads(s) for s in f.readlines()] 29 | return log_entries 30 | -------------------------------------------------------------------------------- /fastapi-server/app/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | 4 | import aiofiles 5 | from fastapi import FastAPI, Body, Request, Response 6 | from starlette.middleware.base import _StreamingResponse 7 | 8 | app = FastAPI() 9 | 10 | 11 | @app.middleware("http") 12 | async def add_to_log(request: Request, call_next): 13 | response: _StreamingResponse = await call_next(request) 14 | 15 | async with aiofiles.open("/var/log/fastapi/access.log", "a") as f: 16 | log_entry = { 17 | "time": time.time(), 18 | "request_line": "{} {} {}".format(request.method.upper(), request.url, "HTTP/1.1"), 19 | "remote": request.client.host + ":{}".format( 20 | request.client.port) if request.client.port is not None else "", 21 | "status": response.status_code, 22 | # "response_hash": hashlib.md5(body).digest() 23 | } 24 | await f.write(json.dumps(log_entry) + "\n") 25 | return response 26 | 27 | 28 | @app.get("/") 29 | async def root(): 30 | return {"message": "Hello World"} 31 | 32 | 33 | @app.get("/empty_body") 34 | def empty_body(): 35 | return Response(status_code=200) 36 | 37 | 38 | @app.get("/test_query_parameters") 39 | def test_query_parameters(deez: str, nuts: str): 40 | return "idk" 41 | 42 | 43 | @app.post("/test_post") 44 | def test_post(hello: str = Body()): 45 | return "You just posted something" 46 | 47 | 48 | @app.get("/really_big_header") 49 | def really_big_header(response: Response): 50 | for i in range(1024): 51 | response.headers[f"X-REALLY-BIG-HEADER-{i}"] = "ha" * 16 52 | return "You just got big 4head haha" 53 | 54 | 55 | @app.get("/你好") 56 | def test_chinese(): 57 | return "Today is very 风和日丽" 58 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | test-client: 4 | build: test-client 5 | image: 50012-lab1-test-client 6 | volumes: 7 | - type: volume 8 | source: nginx-logs 9 | target: /var/log/nginx 10 | - type: volume 11 | source: fastapi-logs 12 | target: /var/log/fastapi/ 13 | - type: bind 14 | source: ./nginx-server/html 15 | target: /var/html 16 | read_only: true 17 | - type: bind 18 | source: ./test-client/result 19 | target: /var/log/pytest 20 | - /etc/timezone:/etc/timezone:ro 21 | - /etc/localtime:/etc/localtime:ro 22 | environment: 23 | - PROXY_HOST=proxy 24 | - PROXY_PORT=8080 25 | # Run basic tests only 26 | - PYTEST_TESTS=basic_test.py 27 | # Run smoke tests only 28 | # - PYTEST_TESTS=smoke_test.py 29 | # Default test options 30 | # - PYTEST_ADDOPTS=-rA --tb=short 31 | # Shorter version for basic tests, only sample a subset of static files 32 | - PYTEST_ADDOPTS=-rA --tb=short --proxytest-nginx-static-files-n-samples=3 33 | depends_on: 34 | - proxy 35 | - fastapi-server 36 | - nginx-server 37 | restart: "no" 38 | stop_grace_period: "1s" 39 | proxy: 40 | build: proxy 41 | image: 50012-lab1-test-proxy 42 | volumes: 43 | - type: bind 44 | source: ./proxy/app 45 | target: /app 46 | - type: bind 47 | source: ./proxy/logs 48 | target: /var/log/proxy/ 49 | - /etc/timezone:/etc/timezone:ro 50 | - /etc/localtime:/etc/localtime:ro 51 | depends_on: 52 | - nginx-server 53 | ports: 54 | - "8080:8080" 55 | - "3000:3000" 56 | stop_grace_period: "3s" 57 | proxy-wireshark: 58 | image: linuxserver/wireshark 59 | network_mode: "service:proxy" 60 | cap_add: 61 | - NET_ADMIN 62 | environment: 63 | - PUID=1000 64 | - PGID=1000 65 | - TZ=Asia/Singapore 66 | depends_on: 67 | - proxy 68 | nginx-server: 69 | image: openresty/openresty 70 | volumes: 71 | - type: bind 72 | source: nginx-server/html 73 | target: /usr/local/openresty/nginx/html 74 | read_only: true 75 | - type: bind 76 | source: ./nginx-server/default.conf 77 | target: /etc/nginx/conf.d/default.conf 78 | read_only: true 79 | - type: volume 80 | source: nginx-logs 81 | target: /var/log/nginx/ 82 | - /etc/timezone:/etc/timezone:ro 83 | - /etc/localtime:/etc/localtime:ro 84 | ports: 85 | - "8000:80" 86 | fastapi-server: 87 | build: fastapi-server 88 | image: 50012-lab1-fastapi-server 89 | command: uvicorn app.main:app --host 0.0.0.0 --port 80 --reload 90 | environment: 91 | DEBUG: 1 92 | volumes: 93 | - ./fastapi-server/app:/code/app 94 | - type: volume 95 | source: fastapi-logs 96 | target: /var/log/fastapi/ 97 | - /etc/timezone:/etc/timezone:ro 98 | - /etc/localtime:/etc/localtime:ro 99 | volumes: 100 | nginx-logs: 101 | name: nginx-logs 102 | fastapi-logs: 103 | name: fastapi-logs -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specific to this project 2 | **/*.log 3 | proxy/app/cache 4 | proxy/app/* 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | cover/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | .pybuilder/ 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | # For a library or package, you might want to ignore these files since the code is 92 | # intended to run in multiple environments; otherwise, check them in: 93 | # .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # poetry 103 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 104 | # This is especially recommended for binary packages to ensure reproducibility, and is more 105 | # commonly ignored for libraries. 106 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 107 | #poetry.lock 108 | 109 | # pdm 110 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 111 | #pdm.lock 112 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 113 | # in version control. 114 | # https://pdm.fming.dev/#use-with-ide 115 | .pdm.toml 116 | 117 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 118 | __pypackages__/ 119 | 120 | # Celery stuff 121 | celerybeat-schedule 122 | celerybeat.pid 123 | 124 | # SageMath parsed files 125 | *.sage.py 126 | 127 | # Environments 128 | .env 129 | .venv 130 | env/ 131 | venv/ 132 | ENV/ 133 | env.bak/ 134 | venv.bak/ 135 | 136 | # Spyder project settings 137 | .spyderproject 138 | .spyproject 139 | 140 | # Rope project settings 141 | .ropeproject 142 | 143 | # mkdocs documentation 144 | /site 145 | 146 | # mypy 147 | .mypy_cache/ 148 | .dmypy.json 149 | dmypy.json 150 | 151 | # Pyre type checker 152 | .pyre/ 153 | 154 | # pytype static type analyzer 155 | .pytype/ 156 | 157 | # Cython debug symbols 158 | cython_debug/ 159 | /.idea/deployment.xml 160 | -------------------------------------------------------------------------------- /test-client/tests/smoke_test.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import asyncio 4 | import hashlib 5 | import httpx 6 | from math import floor 7 | from typing import Callable, List, Coroutine 8 | 9 | from testutils import get_nginx_log_entries_after_time, get_fastapi_log_entries_after_time 10 | 11 | 12 | def test_empty_body(make_httpx_client: Callable[..., httpx.Client]): 13 | """ 14 | I hope you didn't assume that the body always has something 15 | """ 16 | client = make_httpx_client() 17 | response = client.request( 18 | method="GET", 19 | url="http://fastapi-server/empty_body" 20 | ) 21 | assert response.status_code == 200 22 | assert len(response.read()) == 0 23 | 24 | 25 | def test_really_big_header(make_httpx_client: Callable[..., httpx.Client]): 26 | """ 27 | Did you assume that you could read the entire HTTP header in a single socket.recv(4096) call? Oops! 28 | """ 29 | client = make_httpx_client() 30 | response = client.request( 31 | method="GET", 32 | url="http://fastapi-server/really_big_header" 33 | ) 34 | for i in range(1024): 35 | assert f"X-REALLY-BIG-HEADER-{i}" in response.headers 36 | assert response.headers.get(f"X-REALLY-BIG-HEADER-{i}") == "ha" * 16 37 | 38 | assert response.read().decode() == """\"You just got big 4head haha\"""" 39 | 40 | 41 | def test_chinese(make_httpx_client: Callable[..., httpx.Client]): 42 | """ 43 | Mr worldwide :) 44 | """ 45 | client = make_httpx_client() 46 | response = client.request( 47 | method="GET", 48 | url="http://fastapi-server/你好" 49 | ) 50 | assert response.read().decode() == """\"Today is very 风和日丽\"""" 51 | 52 | 53 | def test_cache_stampede_does_not_produce_corrupted_output(make_async_httpx_client: Callable[..., httpx.AsyncClient]): 54 | """ 55 | What happens when a horde of elephants (I mean clients) rush to request the same resource from the proxy at once? Only one way to find out... 56 | """ 57 | # try to find the original on disk 58 | with open(f"/var/html/home_igloo.gif", "rb") as f: 59 | md5_of_file_on_disk = hashlib.md5(f.read()).digest() 60 | clients: List[httpx.AsyncClient] = [make_async_httpx_client() for i in range(128)] 61 | 62 | async def make_get_request_and_return_hash(client: httpx.AsyncClient) -> bytes: 63 | response = await client.get("http://nginx-server/home_igloo.gif", timeout=60) 64 | return hashlib.md5(response.read()).digest() 65 | 66 | client_get_requests: List[Coroutine] = [make_get_request_and_return_hash(c) for c in clients] 67 | loop = asyncio.get_event_loop() 68 | gathered_task = asyncio.gather(*client_get_requests) 69 | loop.run_until_complete(gathered_task) 70 | result = gathered_task.result() 71 | for md5_of_response in result: 72 | assert md5_of_response == md5_of_file_on_disk 73 | 74 | 75 | def test_404_should_not_be_cached(make_httpx_client: Callable[..., httpx.Client]): 76 | """ 77 | Tests that 404 responses should not be cached. We should see two requests in the NGINX log. 78 | :param make_httpx_client: pytest fixture, closure that produces a http client with the configured proxy 79 | """ 80 | 81 | client = make_httpx_client() 82 | start_time = floor(time.time()) 83 | client.request(method="GET", url=f"http://nginx-server/404") 84 | time.sleep(1) 85 | client.request(method="GET", url=f"http://nginx-server/404") 86 | relevant_log_entries = [l for l in get_nginx_log_entries_after_time(start_time) if 87 | l["request_line"] == "GET http://nginx-server/404 HTTP/1.1"] 88 | assert len(relevant_log_entries) == 2, "There must be exactly two requests made after the start_time" 89 | 90 | 91 | def test_post_should_not_be_cached(make_httpx_client: Callable[..., httpx.Client]): 92 | """ 93 | Tests that POST requests should not be cached. We should see two requests in the FastAPI log. 94 | :param make_httpx_client: pytest fixture, closure that produces a http client with the configured proxy 95 | """ 96 | client = make_httpx_client() 97 | start_time = floor(time.time()) 98 | client.request(method="POST", url=f"http://fastapi-server/test_post", data={"hello": "world"}) 99 | time.sleep(1) 100 | client.request(method="POST", url=f"http://fastapi-server/test_post", data={"hello": "world"}) 101 | relevant_log_entries = [l for l in get_fastapi_log_entries_after_time(start_time) if 102 | l["request_line"] == "POST http://fastapi-server/test_post HTTP/1.1"] 103 | assert len(relevant_log_entries) == 2, "There must be exactly two requests made after the start_time" 104 | -------------------------------------------------------------------------------- /test-client/tests/basic_test.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import hashlib 4 | import httpx 5 | import pytest 6 | from math import floor 7 | from typing import Callable 8 | 9 | from testutils import get_nginx_log_entries_after_time 10 | 11 | 12 | def test_request_neverssl_receives_response(make_httpx_client): 13 | client = make_httpx_client() 14 | response = client.request( 15 | method="GET", 16 | url="http://neverssl.com" 17 | ) 18 | body = response.read().decode("utf8") 19 | assert "NeverSSL" in body 20 | 21 | 22 | def test_request_nginx_receives_response(make_httpx_client: Callable[..., httpx.Client], check_proxy_alive): 23 | client = make_httpx_client() 24 | response = client.request( 25 | method="GET", 26 | url=f"http://nginx-server/" 27 | ) 28 | body = response.read().decode("utf8") 29 | assert "netscape" in body 30 | 31 | 32 | def test_request_nginx_body_unchanged(make_httpx_client: Callable[..., httpx.Client], nginx_static_file: str): 33 | """ 34 | Tests that all files return the exact same value from disk 35 | :param make_httpx_client: pytest fixture, closure that produces a http client with the configured proxy 36 | :param nginx_every_file: pytest parametrized fixture, returns one filename at a time 37 | """ 38 | client = make_httpx_client() 39 | response = client.request( 40 | method="GET", 41 | url=f"http://nginx-server/{nginx_static_file}" 42 | ) 43 | md5_of_proxy_response = hashlib.md5(response.read()).digest() 44 | # try to find the original on disk 45 | with open(f"/var/html/{nginx_static_file}", "rb") as f: 46 | md5_of_file_on_disk = hashlib.md5(f.read()).digest() 47 | assert md5_of_proxy_response == md5_of_file_on_disk 48 | 49 | 50 | @pytest.mark.parametrize("request_a,request_b,request_c", 51 | [ 52 | ("", "", ""), 53 | ("", "", "action.gif"), 54 | ("", "", "404"), 55 | ("", "action.gif", "action.gif"), 56 | ("", "404", "404"), 57 | ("404", "404", "404") 58 | ], 59 | ids=[ 60 | "all_request_index", 61 | "first_two_index_last_one_200", 62 | "first_two_index_last_one_404", 63 | "first_index_last_two_200", 64 | "first_index_last_two_404", 65 | "all_404" 66 | ]) 67 | def test_closing_one_client_does_not_affect_other_clients(make_httpx_client: Callable[..., httpx.Client], 68 | request_a: str, request_b: str, request_c: str): 69 | """ 70 | Tests that after two clients connect to the proxy and make requests, if one closes the other should not be affected 71 | :param make_httpx_client: pytest fixture, closure that produces a http client with the configured proxy 72 | """ 73 | client_1 = make_httpx_client() 74 | client_1.request(method="GET", url=f"http://nginx-server/{request_a}") 75 | client_2 = make_httpx_client() 76 | client_2.request(method="GET", url=f"http://nginx-server/{request_b}") 77 | client_1.close() 78 | client_2.request(method="GET", url=f"http://nginx-server/{request_c}") 79 | 80 | 81 | def test_multiple_calls_to_same_resource_should_be_cached_single_client(make_httpx_client: Callable[..., httpx.Client]): 82 | """ 83 | Requesting the same resource twice should serve from the cache. We will assert from the NGINX logs that the upstream server (NGINX) only received one request from your proxy. 84 | :param make_httpx_client: pytest fixture, closure that produces a http client with the configured proxy 85 | """ 86 | client = make_httpx_client() 87 | start_time = floor(time.time()) 88 | client.request(method="GET", url=f"http://nginx-server/") 89 | time.sleep(1) 90 | client.request(method="GET", url=f"http://nginx-server/") 91 | relevant_log_entries = [l for l in get_nginx_log_entries_after_time(start_time) if 92 | l["request_line"] == "GET http://nginx-server/ HTTP/1.1"] 93 | assert len(relevant_log_entries) == 1, "There must be exactly one request made after the start_time" 94 | 95 | 96 | def test_cached_resources_should_be_namespaced_by_domain(make_httpx_client: Callable[..., httpx.Client]): 97 | """ 98 | If I make a request to site-a.com/index.html and then a request to site-b.com/index.html, the contents should not be the same 99 | """ 100 | client = make_httpx_client() 101 | response_a = client.get(url=f"http://nginx-server/") 102 | response_b = client.get(url=f"http://fastapi-server/") 103 | response_a_hash = hashlib.md5(response_a.read()).digest() 104 | response_b_hash = hashlib.md5(response_b.read()).digest() 105 | assert response_b_hash != response_a_hash 106 | -------------------------------------------------------------------------------- /test-client/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import time 2 | from os.path import isfile, join 3 | 4 | import asyncio 5 | import httpx 6 | import os 7 | import pytest 8 | import socket 9 | import zmq 10 | from random import sample 11 | from typing import Callable, List, Generator, Optional 12 | 13 | 14 | @pytest.fixture(scope="session") 15 | def proxy_host(): 16 | return os.environ["PROXY_HOST"] 17 | 18 | 19 | @pytest.fixture(scope="session") 20 | def proxy_port(): 21 | return int(os.environ["PROXY_PORT"]) 22 | 23 | 24 | @pytest.fixture(scope="session") 25 | def proxy_address(proxy_host, proxy_port): 26 | return f"{proxy_host}:{proxy_port}" 27 | 28 | 29 | @pytest.fixture() 30 | def check_proxy_alive(proxy_host, proxy_port): 31 | def check_proxy_alive_closure(): 32 | attempts = 1 33 | while attempts <= 10: 34 | try: 35 | with socket.create_connection((proxy_host, proxy_port), timeout=10): 36 | return True 37 | except OSError: 38 | attempts += 1 39 | time.sleep(2) 40 | return False 41 | 42 | return check_proxy_alive_closure 43 | 44 | 45 | @pytest.fixture(scope="session") 46 | def zmq_socket(proxy_host): 47 | context = zmq.Context() 48 | _socket = context.socket(zmq.REQ) 49 | _socket.connect(f"tcp://{proxy_host}:5555") 50 | yield _socket 51 | _socket.close() 52 | context.term() 53 | 54 | 55 | def try_and_get_response_from_zmq_server(zmq_socket: zmq.Socket, timeout: float) -> str: 56 | response = None 57 | time_elapsed = 0 58 | while response is None and time_elapsed < timeout: 59 | try: 60 | response = zmq_socket.recv(zmq.NOBLOCK).decode("utf-8") 61 | except zmq.ZMQError: 62 | time.sleep(0.2) 63 | time_elapsed += 0.1 64 | continue 65 | return response 66 | 67 | 68 | premature_exit: bool = False 69 | 70 | 71 | @pytest.fixture() 72 | def restart_proxy(proxy_host, zmq_socket: zmq.Socket, request): 73 | def restart_proxy_closure(): 74 | zmq_socket.send(f"begin_test {request.node.name}".encode("utf-8")) 75 | response = try_and_get_response_from_zmq_server(zmq_socket, 15) 76 | if response != "restarted": 77 | global premature_exit 78 | premature_exit = True 79 | pytest.exit("The proxy runner was not able to restart your proxy." 80 | + " Check the logs of the proxy runner container and restart it if necessary.") 81 | 82 | return restart_proxy_closure 83 | 84 | 85 | @pytest.fixture(scope="function", autouse=True) 86 | def setup_proxy_per_test(proxy_host, proxy_port, check_proxy_alive, restart_proxy): 87 | restart_proxy() 88 | assert check_proxy_alive(), "The proxy does not seem to be running before the test starts" 89 | yield 90 | assert check_proxy_alive(), "The proxy seems to have died after running the tests" 91 | 92 | 93 | @pytest.fixture(scope="session", autouse=True) 94 | def cleanup(proxy_host, zmq_socket: zmq.Socket, request): 95 | def send_end_tests_message(): 96 | if premature_exit: 97 | return 98 | zmq_socket.send(f"end_tests".encode("utf-8")) 99 | response = try_and_get_response_from_zmq_server(zmq_socket, 15) 100 | if response != "ended": 101 | pytest.exit("The proxy runner was not able to restart your proxy at the end of the test suite." 102 | + " Check the logs of the proxy runner container and restart it if necessary.") 103 | 104 | request.addfinalizer(send_end_tests_message) 105 | 106 | 107 | @pytest.fixture() 108 | def make_httpx_client(proxy_address) -> Generator[Callable[..., httpx.Client], None, None]: 109 | clients: List[httpx.Client] = [] 110 | 111 | def httpx_client_closure() -> httpx.Client: 112 | proxies = { 113 | "all://": f"http://{proxy_address}", 114 | } 115 | client = httpx.Client(proxies=proxies, timeout=5) 116 | clients.append(client) 117 | return client 118 | 119 | yield httpx_client_closure 120 | for client in clients: 121 | client.close() 122 | 123 | 124 | @pytest.fixture() 125 | def make_async_httpx_client(proxy_address) -> Generator[Callable[..., httpx.AsyncClient], None, None]: 126 | clients: List[httpx.AsyncClient] = [] 127 | 128 | def httpx_client_closure() -> httpx.AsyncClient: 129 | proxies = { 130 | "all://": f"http://{proxy_address}", 131 | } 132 | client = httpx.AsyncClient(proxies=proxies, timeout=5) 133 | clients.append(client) 134 | return client 135 | 136 | yield httpx_client_closure 137 | loop = asyncio.new_event_loop() 138 | client_closing_coroutines = [loop.create_task(c.aclose()) for c in clients] 139 | loop.run_until_complete(asyncio.gather(*client_closing_coroutines)) 140 | loop.close() 141 | 142 | 143 | def nginx_list_static_files(n_samples: Optional[int]) -> List[str]: 144 | nginx_file_path = "/var/html" 145 | list_of_files = [f for f in os.listdir(nginx_file_path) if isfile(join(nginx_file_path, f))] 146 | if n_samples: 147 | return sample(list_of_files, n_samples) 148 | else: 149 | return list_of_files 150 | 151 | 152 | def pytest_addoption(parser): 153 | parser.addoption( 154 | "--proxytest-nginx-static-files-n-samples", 155 | default=None, 156 | type=int, 157 | required=False, 158 | help="if set, samples n files for testing with the proxy" 159 | ) 160 | 161 | 162 | def pytest_generate_tests(metafunc: pytest.Metafunc): 163 | if "nginx_static_file" in metafunc.fixturenames: 164 | n_samples = metafunc.config.getoption("--proxytest-nginx-static-files-n-samples") 165 | metafunc.parametrize("nginx_static_file", nginx_list_static_files(n_samples)) 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 50.012 Networks Lab 1 Test Suite 2 | 3 | This project implements an automated test suite for the HTTP proxy lab. It spins up a few test web servers in a docker network and a test monitor that starts new instances of your proxies on demand. Finally there is a run-once docker container containing a pytest suite that that communicates with the monitor to test your proxy. All these docker containers are orchestrated as a single docker-compose project. 4 | 5 | ## Usage 6 | 7 | ### Installing your proxy code 8 | 9 | Place your proxy Python files inside `proxy/app` (create the folder if it doesn't exist). You can have as many files as you like but your application must conform to the following requirements: 10 | 11 | - There must be a `proxy.py` file as this will be the entrypoint called by the monitor. 12 | - Your application must start the proxy on port `8080` when no arguments are passed via the CLI. 13 | - Your application must clear the contents of the cache folder (if you save it on disk) before serving clients on the HTTP proxy. 14 | - Your application must handle `SIGINT` gracefully, releasing all resources and exiting within 5 seconds. 15 | 16 | ### Starting the environment 17 | 18 | Create the folders for the bind mounts, you only need to do this once. 19 | 20 | ``` 21 | mkdir -p proxy/logs && mkdir -p test-client/result 22 | ``` 23 | 24 | Then deploy the compose file: 25 | 26 | ``` 27 | docker compose up 28 | ``` 29 | 30 | Deploying the compose project automatically starts the full basic test suite (no smoke tests). Wait for the `test-client` service to exit. In another terminal window you can watch the stdout of the test-client using: 31 | 32 | ``` 33 | docker compose logs test-client -f 34 | ``` 35 | 36 | ### Viewing results 37 | 38 | #### Overall test results 39 | 40 | The overral test results indicating test successes and failures will be saved in the file `test-client/result/result.log`. Only the latest invocation's results will be saved. 41 | 42 | #### stdout/stderr for each test 43 | 44 | A fresh instance of your proxy is started for each test that is run. You can go to `proxy/logs/{test-name}.log` to see what was printed to stdout and stderr by your proxy during each test. Only the latest invocation results will be saved. 45 | 46 | #### Latest cache results 47 | 48 | The cache directory will be left in the state of the last test run as it is cleaned at the start of each test. If debugging errors it is suggested you run one test at a time using the `-k` argument in `PYTEST_ADDOPTS`. 49 | 50 | > If you open a log file in the editor and re-run the test suite, you need to reload the file from disk to get the latest version. 51 | 52 | #### Proxy Monitor Log 53 | 54 | The monitor is the process that runs your proxy repeatedly on demand. You can inspect the logs of the `proxy` container (`docker compose logs proxy`) to check for any abnormalities, such as detecting the TIME_WAIT bug (see below). 55 | 56 | ### Running the tests again 57 | 58 | You can edit the files in the `proxy/app` folder directly if you are working to pass the tests. When you are ready to re-run the tests, restart the `test-client` container: 59 | 60 | ``` 61 | docker-compose start test-client 62 | ``` 63 | 64 | #### Running a different suite 65 | 66 | By default, the test client only runs the `basic_test` suite. To run the extended smoke tests, edit the docker-compose.yml file and swap the commented lines under `services.test-client-environment`: 67 | 68 | ```yaml 69 | 70 | # Run basic tests only 71 | - PYTEST_TESTS=basic_test.py 72 | # Run smoke tests only 73 | # - PYTEST_TESTS=smoke_test.py 74 | ``` 75 | 76 | Changes to the compose file require you to update the deployment, just run `docker-compose up -d` again 77 | 78 | #### Different test arguments, speeding up the test 79 | 80 | The full test suite that will be using for grading tests every single static file in the `nginx/html` directory. When doing iterative testing this can get quite cumbersome. In the `docker-compose.yml` file, you can pass the add the pytest opt to control how many samples you want to test: 81 | 82 | ```yaml 83 | - PYTEST_ADDOPTS=-ra --tb=short --proxytest-nginx-static-files-n-samples=3 84 | ``` 85 | 86 | As above, changes to the compose file require you to update the deployment, just run `docker-compose up -d` again 87 | 88 | #### Running a single test 89 | 90 | Use the `-k` argument in `PYTEST_ADDOPTS`. See pytest documentation on [run tests by keyword expressions](https://docs.pytest.org/en/7.1.x/how-to/usage.html#specifying-which-tests-to-run). 91 | 92 | ### Troubleshooting & Debugging 93 | 94 | #### Test Client Hanging / TCP TIME_WAIT 95 | 96 | If the client does not proceed for more than a minute, check the logs of the proxy container to see if the monitor has crashed. Your code might be suffering from the [TCP `TIME_WAIT` bug](http://hea-www.harvard.edu/~fine/Tech/addrinuse.html) if the monitor reports something like this in the logs: 97 | 98 | ``` 99 | WARNING: still found port used by process None in TIME_WAIT state 100 | ``` 101 | 102 | If the monitor has indeed crashed or stopped responding without any other errors, it might be a bug in the monitor. Raise issue on Github & contact your TA. 103 | 104 | In any case, if you need to force abort the test, you can stop the test-client container, then stop the proxy container, then restart the proxy container, and finally restart the test-client container when you are ready. 105 | 106 | #### Wireshark 107 | 108 | A wireshark instance served over RDP is available in your web browser at `http://localhost:3000`. It runs on the proxy instance. So for example, you can capture all downstream HTTP-in-TCP datagrams using the packet filter `tcp port 8080`. 109 | 110 | If the application stops responding, restart the `proxy-wireshark` container. 111 | 112 | ## Test Technicals 113 | 114 | ### Static Website tests 115 | 116 | The `nginx-server` container serves static files from the `nginx-server/html` directory. You can add your own files to test here. The parametrized test `basic_test::test_request_nginx_body_unchanged` samples a list of static files in the html directory and tries to fetch the resources. 117 | 118 | ### Smoke Tests 119 | 120 | The `smoke_test` suite is an extended suite for extreme edge cases. You will not be graded based on your results on this, but you can try it for fun. 121 | 122 | ### Your own tests 123 | 124 | Feel free to add your own test files under the `tests` directory. 125 | 126 | ## Development 127 | 128 | First developed by Chester Koh for the Spring 2023 Term. 129 | 130 | ### Things to update for future batches 131 | 132 | - More basic tests, such as caching query parameters 133 | - Bonus tests for extra features like ETAG and HTTPS 134 | - Test downloads of large files (scale of megabytes) 135 | - Test for TIME_WAIT bug 136 | - Rename the `proxy` service in docker compose to `proxy-monitor` for avoidance of doubt. -------------------------------------------------------------------------------- /proxy/monitor.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import psutil 4 | import signal 5 | import subprocess 6 | import zmq 7 | from typing import Optional 8 | 9 | context = zmq.Context() 10 | socket = context.socket(zmq.REP) 11 | socket.bind("tcp://*:5555") 12 | 13 | print("Bound ZMQ socket, launching idle proxy") 14 | 15 | 16 | def wait_python_proxy_ready(pid: int): 17 | python_process = psutil.Process(pid) 18 | time_elapsed = 0 19 | while True: 20 | python_connections = python_process.connections(kind="tcp4") 21 | for connection in python_connections: 22 | if connection.laddr.port == 8080: 23 | return 24 | time.sleep(0.1) 25 | if time_elapsed >= 10: 26 | raise TimeoutError("Python proxy did not managed to bind to port 8080 after waiting 10 seconds. " 27 | "Check the individual log file.") 28 | 29 | 30 | def launch_proxy_process(experiment_name: str) -> psutil.Process: 31 | logfile = f"/var/log/proxy/{experiment_name}.log" 32 | p = subprocess.Popen(f"exec python -u /app/proxy.py 2>&1 | ts > {logfile}", shell=True, 33 | stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd="/app") 34 | return psutil.Process(p.pid) 35 | 36 | 37 | def launch_proxy_and_wait(experiment_name: str) -> psutil.Process: 38 | shell_process = launch_proxy_process(experiment_name) 39 | print(f"Launched sh subprocess as {shell_process.pid}") 40 | attempts = 0 41 | while not shell_process.children(): 42 | print("Wait for sh subprocess to spawn children...") 43 | time.sleep(0.2) 44 | attempts += 1 45 | if attempts > 10: 46 | raise TimeoutError("sh subprocess did not spawn children after a reasonable time. " 47 | "Check if proxy.py exists in the app folder.") 48 | try: 49 | python_process = next(filter(lambda cp: "python" in cp.name(), shell_process.children())) 50 | except StopIteration: 51 | raise Exception(f"Could not find a python child process - it might have died immediately." 52 | f"Check the log file {experiment_name}.log") 53 | wait_python_proxy_ready(python_process.pid) 54 | print("Proxy ready") 55 | return shell_process 56 | 57 | 58 | def shutdown_proxy_and_wait(shell_process: psutil.Process): 59 | python_process = next(filter(lambda cp: "python" in cp.name(), shell_process.children())) 60 | print(f"Sending SIGINT to process {python_process.pid}") 61 | python_process.send_signal(signal.SIGINT) 62 | time_elapsed = 0 63 | while python_process.is_running() and time_elapsed < 5: 64 | time.sleep(0.1) 65 | time_elapsed += 0.1 66 | while python_process.is_running(): 67 | print(f"Process {python_process.pid} is still running, sending SIGTERM") 68 | python_process.kill() 69 | time.sleep(1) 70 | # check if port 8080 is free 71 | port_available = None 72 | time_elapsed = 0 73 | while port_available is None or port_available is False: 74 | port_available = True 75 | for connection in psutil.net_connections(kind="inet"): 76 | if connection.laddr.port == 8080: 77 | print(f"WARNING: still found port used by process {connection.pid} in {connection.status} state. " 78 | "Retrying in 1 second...") 79 | if connection.pid is None: 80 | print("WARNING: netstat reports the port is being used by an exited process - " 81 | "did you close your sockets on program exit??") 82 | port_available = False 83 | time.sleep(1) 84 | time_elapsed += 1 85 | break 86 | if time_elapsed > 5: 87 | raise TimeoutError("Port was still not free after 5 seconds :(") 88 | 89 | 90 | current_shell_process: Optional[psutil.Process] = None 91 | 92 | try: 93 | current_shell_process: Optional[psutil.Process] = launch_proxy_and_wait(experiment_name="pre-experiment-idle") 94 | except Exception as ex: 95 | print("FATAL: Could not start the proxy before experiment starts:") 96 | print(str(ex)) 97 | print("The proxy runner will still be available, " 98 | "but it is recommended to investigate and replace the proxy.py file.") 99 | 100 | sigterm_caught = False 101 | 102 | 103 | def on_sigterm(signum, frame): 104 | global sigterm_caught 105 | print("Caught sigterm") 106 | sigterm_caught = True 107 | 108 | 109 | signal.signal(signal.SIGTERM, on_sigterm) 110 | should_say_waiting = True 111 | while not sigterm_caught: 112 | # Wait for next request from client 113 | if should_say_waiting: 114 | print("Waiting for command on ZMQ socket...") 115 | should_say_waiting = False 116 | try: 117 | message_b = socket.recv(zmq.NOBLOCK) 118 | except zmq.ZMQError: 119 | time.sleep(1) 120 | continue 121 | should_say_waiting = True 122 | msg: str = message_b.decode("utf-8") 123 | code, data = msg.split(" ", maxsplit=1) if " " in msg else (msg, None) 124 | print("Received", code) 125 | if code == "begin_test": 126 | print("Beginning test: ", data) 127 | if current_shell_process is not None: 128 | try: 129 | shutdown_proxy_and_wait(current_shell_process) 130 | except TimeoutError as ex: 131 | print("Could not kill the existing proxy instance at the start of test:") 132 | print(str(ex)) 133 | socket.send("could_not_end".encode("utf-8")) 134 | current_shell_process = None 135 | continue 136 | try: 137 | current_shell_process = launch_proxy_and_wait(experiment_name=data) 138 | except Exception as ex: 139 | print("Could not start a new instance of the proxy at proxy at the start of the test") 140 | print(str(ex)) 141 | socket.send("ended_but_could_not_restart".encode("utf-8")) 142 | continue 143 | socket.send("restarted".encode("utf-8")) 144 | elif code == "end_tests": 145 | if current_shell_process is not None: 146 | try: 147 | shutdown_proxy_and_wait(current_shell_process) 148 | except TimeoutError as ex: 149 | print("Could not kill the proxy instance at the end of test suite:") 150 | print(str(ex)) 151 | socket.send("could_not_end".encode("utf-8")) 152 | current_shell_process = None 153 | continue 154 | try: 155 | current_shell_process = launch_proxy_and_wait(experiment_name="post-experiment-idle") 156 | except Exception as ex: 157 | print("Could not start a new instance of the proxy at the end of experiments:") 158 | print(str(ex)) 159 | socket.send("ended_but_could_not_restart".encode("utf-8")) 160 | continue 161 | socket.send("ended".encode("utf-8")) 162 | else: 163 | print("Received unknown message") 164 | socket.send(b"???") 165 | 166 | print("Closing ZMQ server socket") 167 | socket.close() 168 | print("Closing ZMQ server context") 169 | context.term() 170 | print("Goodbye") 171 | -------------------------------------------------------------------------------- /nginx-server/html/m.d.html: -------------------------------------------------------------------------------- 1 | x��<�s�F����!��dל�gl��e� x&�[[�Fj�cI�O-���~�k�Hv<�2Sq� ����>Z�xw5��?Otr;���}o8�J�^�tدׯ�Wa�a�U��JG��{�һW�r1̇z�� a32���b�k�C_��A]vQ���w�� �'U���/+�AV��*�?������zڬ5����e���y���nk��FMx��G}��Ѭ�}� ;���C|/`ఙ�GSLg�>'#��kҵ�� Jf��0/��E=�!|�����]�`���RxE��: '��<;;�5ڵ��qz�<�6���{r�^'5�J�Y�呟� �6�g���?W�Ag/�,���ٓ���o��ɔY�Jf��#�g��|�?����o�ȕ�:��&�>ؤ�wϭ�0��W�;�e�a�:�b��'�ɥ����U�ٹF�;��� ���2�S��sR[8�qWLwU�K�lhg�i��f`�d���������č>ҧ��x�]@|�E뀫a��?��_��2�N�DZ���)�G��?�����P��]��W��\�&���~*�w��Sxr�~*d8�u�ۏ���u8��w?ԯ�w���`tY���x��u4�bҹxW����a�;}�ͻ�y�ڹ�O�q�� u�ׇ�٤��n.�nҽ� �> �淗�f��c���S����Ò�� ]��G��ft t.w7d6�_V֠�����K���I��w:@KF�|��N� ��7���f�$��}|ڹ�{����T����a��=�x��[�_H8�UR���6��xz�OaS�J�f�P�q�f�qCf�u�-i`�5*���ޙu#N�/�x��[P2���r>�V C��¾�g�� �!"�˅B�:��U�'/,s��[�cK�y�z�Z������Ŏ�����.5����R�>�1�+��Wً�1f�Ə��FvD�~��0}��S��Wp�-��[��,b�kPM�����y&u��z�Z�Y� �D�f���/�-�Fx�I�po�9U;�������i���'��H�${��4�J����4��`vם���_��M���!^��Js�������C2JN]���'�x��]뵎���|���ò*o� D�p+��rf��"��n�o���ud�Vh�tnFB��T�r�H����W<����/����oSF�b_$�g������m�nf�2��6��xc%z�ߏ��Q9�B�,] ��� 5H�X��n������<�n8�� �^�&z+�+Ņ#��*��,�s>��D�z��%u9�`�{5Ӳ~�����I�X���pv���}�������j��>�5pi����:�<��x����QS ������q���1)�`2i�Zh?l�npJ�Zւ�S:zXFkl�z��%�w���>[�2���4}NJ����j�n���tr0�l#��+�؆Y�M���l��2 �e�2��'f>߰�7�#D9��G%v} ���ۊ�{���@v�Ͽ9��rg�G��a t/#��:�]r��2���[���~s�D�*�0yZ��}7�ts� ��5v0��0i���`k������{���� ���[���j�V>� ]����Y��������#=��ΑF=Ol������K�W8} "� �����$���[�>3��zf�<~@��2Ys���ĹAB-)4XX-�&��JE P���S`B��%���`=nY��L~ )�%6c��U]��k���q�Q\�NU���3u ��o�SDNP ��F�e�� [�o��h!Nt!�p�[Z�[ mk uH����Ӛ/�/dh��RV�P�w��'t�-��� j<���Q��,��kq�r՚��h� ����c�\�ȋ����Q����^X���La+�}x�<;tGu�}T�C�4V��������r"v�{��x��R��^[�Y&����\��n"`�� �P��1��'Uj����/����r�:]@A?�J�r��]��Rj�J %U�������j�y�\��"H��� !�1fx ��S��ny,�3��KD2��&���$�p ��V��_�? R�-F *�2k| ^>��_G�BXR�rp���� 2�K���tY���' a��Za��sQWX(.k!I������a�sœ6� �9�d8|�B�������:q>Fs�r &{�~���@re(�U���1�j[�X�EC��U�9��cψқ��<4fd� �6 |N�. \�=HQ ��H Ԗ��� ���`��� 7������z:�%e ,���QtW�#^U�+���l5C�Z�����$� j�V|R��P��0\�-��(ȸ��uX��>��M��b�Q+��!xS���j�s�0F:��G�X���_�_R�!�]�b0�l�[��ٱ�����jg�c�)J�i��͞�� ~� FJz�������gV Y�"g&��N�z�%W5M����-yW)бjT��x%����<%"{fc_%F����3`~���0$�� n{����Ho'�Tp��'{H��#djd��Ô3㉮�ӻ�+*FN��C�?�h�+��~8L�z�����y�^G��9�j�z�Ӎ��B/����U�����Z0k���«�<�Q'髋� ��łQGu����0���]\c�ű�7� ��~tY��E���,m�������hSﱎ�LuU��O�8��. �.�c��WW�� 3\�an�^췗��6�� ��t� K��[����Qh񷁳[Zh'E�m�߅�9�z�n�Uq��(����(��s��f�ޟ����?����F�6y%�����x-�Sd�!fɓ��ϳ�0xb�"�V��l�� ]��Ea���� ����!:�&��f�\��(�+L���}����W����t�s����HS�1B�MD)�0 ���z�������ח���G�7�貴s��q'u]XFB����yf��;�wq� �Z!e�e&+2��(�K�z�)�+L�S�U�j�. 3(o�G7�w�La�H�t�f>V,�}�`P�����_E �Ȯi9_�!�����{��&J%6C�dz�dojB�e���T�,X�ܯ(0�w��3�bth/�^v�T7뙇q�|��3v��Z���ݦ�V+I���_+.8�E׶p��z<�gO��i����o�}�$i��O��+�u�S��Gx£С[��rM�.��� ��J�qprx�jW�R�]"�h���`�7� ���fv&&!�}aT�݂a��i�0�?`�����?9R�sl�/��騚�)���/�|r 3���� �������Ә�s_*s+���'٭S��`؞�� ��QǹQ�a���W:4��aYTI��:�H�T���~����#`&-�8�糽a�N�������� T3��t�$d�M���9�RU�:��X��9y�D��I��3Z ��[�]/�T���Y;��^�T�n@FW��Gz��3�k��G���OI��9�� ����c2�����I_�߸Mz� K�Z��^�E�uU��������� �[xX|��.�-?GBU1r�eX��S� OK���2����$�� g��T}�3�J�Cd���a ��N�/`�~U5 pv\��%q{S����ewu %o\aB8p�����(a'�0?%Ë��A��a6���a�٪�2v"�`Yd���oe�y���J�gz�r�+<��J,������� d��¢,Vy��|(5LL �s, <Ă�{Uitx_�N!�1�s�I����:^C��<}+�@�4 �<����M�B,cWk� ��{퓢T�<�$�a���m�؇[�W`r��#w����ÿ�f��ž�V�L#&Ԅ��H����4 %�!ΠG��o\�u��rxTfWj ��K����^ka9V�#�⌰�0Q���� @w���;Q��y�}I3��+���Y��-���ݧq��A�"�IaQ�Z��+�������� h�������W3�g���Š� "q �@)6m l��1���+h^S� ����^E�a^���K��Pg�@q.�*ꞯ��(����8�(d5�%�ȥ�؁F�(Xu˝����jġ�R:$ⰇV�Qw�`�Y_D�@�[��L*~H�uO��*W ��8�� M1,���7L�]��9��JZ�^;�G��p��� �?'W���,�)\�JoGΡ��RҨ�j�Z�[�a ɾ�Y�4��J�,Qk�8/T�l����w2��<�a�v�`�T�.]��`��o��U�:e��Ly��?�=^�<�̯��5w���1�&�R0�K�$_G(�Iu�%\��� �"���F9a�瘥׹��z�:��Q���y�Jt�V�$Q��U������R��@DŽ� m���f2K���N��[�"���ځ���3�LW���o , �_ �6HA���h3��A�� �2��`��E�jE�5�� �4 "_�]��=�!�_��8��a#��<�Q�}��I]e�^�H �w/�$JE=����gU���@�Z=~�J�]����̍��J�0� 3��ë!D����W'xF��- sGl���b��i���!s��xv۽�;Y��7}��~*��=v��'\M���6�M�A�k�8n2̒��d앹��?��v�Mb�q��p���Ⱥ�<�\p�ˤkK9ZJ~(\�Ea�t~���)@";9h�’��т-]�Pވ����:��SH��)��(�G�2 �c����Ww��m�V�;�v�ݩ���yL��e�֬mi%9i>y(��S$���U}�I�!j��I;��X$@����� Ey��R�=��^�٬���Eڜ��ȦX�H� ��=��=K,���V@��U)�B��'���z� L�}�֞L�)w�$S�Qc�{���9�-� c���.��@��~���A��5��z����u�#y�2?�w��C�ᗣ/-�4�۠p�*��L����Y*i��b���� ����{�y���x���F������f�g k時�� S�Ԣk[��aU����[| L������L����=]B�շ':�w��x)�Ѷ�hPcgeo�MMP��D�˝��t��0rcGCo�K�pF��q�����#UW�݂�`���Ƒ�3[%A��W"���6۠ᇞ���� �"H� ���ӷ7(u-=,�jUgIڣ�� clg�]�1EǷ�:L9��.ɧd} ?�}��M����j�&4�G�Ǡ�2�w�22gDE�頸�� 7�'������&���]N����`������-v�l����x]�C�]����]� �f�����:�2�/�Tݔ��Qb�[ƥ~����J��zhF�����J������%��ܽB^���pԺ�h .�?������șr������R���{����� ���C�����~tF]?:����(C���7���M�wл��;�?�5����ѡ��ơ�E���c,�����ժ�m�b �k[�"��qB�읬� 6��/���w���i��"�u�Pcw%. ���G��GN�^H��%�@�Lp�Q.rSF��MG.Hdy�@<�O���=~ܰq��t�#@(��(olT�Cœ5!���%�_�R��E��6�oS �k~ X#�"�0��&� �?ѹ3W�&��er �Xp����1��ff}dž�{8��M1U��Q ^j��7��b.CF�,u�NH �[���r�H1�K,�Y8^>��c)=!F�f3Z-��b_d���w����&=:�%�m�kXJ� �����{kG����� pq|��O�yc���Ͱ����e�S�]?ш��*^E������e�3�\'Iɕ��)� ��N2� �aM���G ;_�0�S����_a&�i4��¡1����ߏ�}��6F�:���� S�Q�4�� w�k�1�ꌌ�!���*BVeDEw&k���R &v�% (�~�ԠLr ��7-���[/�4%��x�{{��eUM!����vŘ�����2��ra�6�tє��JW}�8)�Nz�-�)cGФR�,��'�����8s��w{9�r�l�A��ل�3G!�S���$��F�'s�C��ǫ#�ֹ���e>q�.��Nkt?�$��,�N��J!�{���C�4z���~�c�����^���Z#��Pf`�-��rE&���nQؾ"� c���e\mz�^�X@(b����;�RA�b��K���1���� ��6�f������D�c�fI`+�pK�x�/��-r�a:�����)"E`藂��*���I��� �j�����@��_K�=���N�a�Z��tM� g�eiEk��/;M�O��Y�2�e�J��=���S�'��1>�c���W�Ht��9zh���Kv�a#�I�Mp�La�a�ٜ��r(Us��Eƀ�(gA����f^`�тF�(U�E0c� .�����q��T���E�پ��DE�yţ��8B���,o7����K�����*h��o+����.�ꆊ~`#����@?U�� �i3d`���jˌQ�� Q��LG����1,�0İPf�ǪvL�o�4�6yE����!�E�����|E���{5��+�.���j�*G Z6�lk�8>mm5Q�mֆ_���x����m�qS�j{sC ��& ؕ�e�' _f�U �4�2.�bmy���%�>��]��/�����.I��{����}��Kt,h��]��'r��am�H��pb�u8T��\�[O�Z�*-�� 9L�5&��$�*�L>�)�א� �Hd QK7��z �2�!X.4Dz�s8 ��L:?3��֠��(?{i ��O|�ZU��{�|���ɠ����T?H�L`R��N8�C� �!��Ƀ��#a��}l���$�W��7�@�sn૽i�����<PE�A�4�Z�� �m%{��0�ل��@��!%�Y������!VeV0�&Ϩv��0��Et�-g�C,��s�=bkZ���pO �P��G��4Dy'�u�QJ������ \�B;�N��~�A*�P�G�$(�H����b�|8� ���͍(�O�D�z��2�u�� ���w2F��Hn��G��n��s(q?�s������9|�ł���xn�:z2՘��d/��YU���񆙞Z� �0�,%�i`�w0:*��)�����1x��:5cF����zy� K���]�JBG&�tԹ��Qb�(���-p�K����YQ�1[Uծgڷ�� �Ԣҥgd��T=i`�ţ� :<@��P:L�Ӵ�!p���H3�B��$��p�("S�f�q��,[4�D�"^Y���$�~�~A���b�O�K���t��6�}0+�l�y{ +�$1�|X�9�(/WI�"Ace�Wd����U��KÛٵj@?[�ѝ� Y��A��m���E���2 E�6x5�a*^��#9�LgG9�x���A�3t�#]��ء��Y>p�,�^��{��;aTB4gz䙤uu�u��+R{�zS���dul0��� W7} �Ǟp��;3��2��@sy쬐P�V���d�➆F�p�iK����cM�w��Hh!�6X�߂ �D4��M�jrh6(�F�>�t�0�h�Yr��� ��_W�$�u�.;� �0u�A���tT�C��q��\�ӡ���Z�G ��&��Y��� �_<��UT���8�u����ђ�_@:S<+.0���K�^9aк�hI?�ׇ`[��+���"D+ �\���u� ��ς�͚ ��5 .I���" �S;�r�/Q�F����e?�J����Z �}ż�����}m��eFL{+� ��"f{]_Ev�p'qQ:��`D�"�ir��E� 󈶜���Y-�������e|�_K ��w4a�h^�d�oF��RA(㙺S��ip�7�>jYWf/HH�n���F�����D���)�� '�$� I2�)" ��9G�;�)_>��sE%G"����J�X�����XT���8?�}J�4��5#��6/�_����Vo��s�77�}q�T�����G����>�+5�7�}"ᰴ��:�M ����[j��Ϲ�L��G�����Y��P�"��=oiƑ��̥.y���1s݅��z�/�U���ڱ�|-����ص� ���,�?��ͷY��p��x�"u]�<,r��̊�ݪ{��g��MȜ�o,�V���5�\��+ g,a �Ҳ���"�@�[�퍴���1c�J����;�X�޶bּ'Xx�1���WѰ�Nka����;��ԧ' �`� 'h��"���n���� � Aс�D0 �J)B|�TE/��c���o {�� fk�h) �_�F�L�|s��_ '9<c�h�����l��;����<+9M1��a\څZ%ф�e[�E���O}�[d%��M���ZCf�g��-�s�4�bW�8��ne�,�F�ژrQ����Ђh�'��p��KFr��%B:ݢh ��5�pi�D$�8�*9��Q�Q䠋��$V��:���/�?���R��+r�=k��U��k�j�w�T)�W[�7$�T����$�=�^�>��,��C�Z�)̃$y�B�����V����ő���29��H��T}d"�����4���8�1k�<�xm0j�ZDX1����^кߗ��ͷ�'2�����H3E@F+Fb�K� Ƿ(����Z��֯���2����WI56����=�]_V���(��K����_� �U v��SU�����D���-�P-g��b�?�cdP��� ӏ3��:�d�zh6I���-�R�δ�U��X�^&?)�z��8�#��C�^(�J��F��OBRB�Q�-c���>"�r蚎s�EWD����,7��� p�v�$��4��Ϲs؛�r�9��Y��g��>�6u�����M2�O��l?~�l>\D�;�����H��M��k>���xʔ���L�����2�mt⏻� i^��D����H�� ќ�K���z<��%[��#M�,UR���, `��o�]5�Z.�N虒���K�����Z ���"�2���u�: ���E�Os9O8�uY�r�����,��j�KCѽzf~� ��eWx��߇+X��q�mt%����A?HԊ���� �e��`�\MK��v �ɍB1�/ Bl�g��KPƘ��$� JF���tY���v����'�| }�anT���9��%W�'�� UB�@ EOKP[� thnY򡯾�����Е"�j����s��w��*�A�$�|0!�5Ć��F�b�D�.����9 w�6~�g��CQ�%W]{��|���PTb��j��ڝ<��#�G��~\��J�����#�Ո�ݽ��ӕ�������<��B����,“̜��,�cu�tH=? �'1M�zWbmK�ڟy �!OC@}��������yMa�p��r^�'�+6��y����ܤF�����³2�q�M{���&fq7���I��fny�Z)"R]G���+Du��,����`ig��t��U�K����rq (g�Yk�';r�jle�!��qX)��W�l� �S�`=+�>zp��8^�l@!�TI5F.��f���Oj�^|�����������|s� "Y�����O�3U�$&�����1�O�8���c N�l�,$�ڧgbg(��� �J���1H[�g$7�<���7�:�������(3�z��mH���ͅ���x?�D.��J�i�x���A:RhԮt�ˋS���G��"�ر�?�nu�hf|>3��tz�̡���@h�刧 /�RY���� ���m��^�\U6h��� θJ�*Z�֕�w2PA/#Z34�0��bQ�Q��v�٪��&G����o iv 1�� nݱ%ܺ�>�h(���f���w�4���&t��9�#�N���c�t[�\��&4m�dږ�+�j�`_GG'��ٵ�>���BLJ���NS'��p��S���T�qz}��q�W�VI��ـF�|]F�(47�4y>�7͓��ǧ{G'''Vr������Gq�Ly�ߞ�#֝�����7� �t�A�by2�[?���T0x��Q�S�Y��w�@&/� z<��y����?`�w5�8z#�gcP�ȍ���.�⮼�ޜY�B�-5 -------------------------------------------------------------------------------- /nginx-server/html/microscape.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Welcome To Microsoft 5 | Welcome to Netscape 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

17 | 18 | 19 | 20 | 34 | 35 | 36 | 37 | 42 | 43 | 44 | 45 | 46 | 47 |
WELCOME TO NETSCAPE 21 | Microsoft HomeProductsSearchSupportShop 33 | Microsoft Home 41 |
48 | 49 |


50 |

51 | 52 | 53 | 54 | 57 | 58 | 61 | 62 | 65 | 66 | 67 | 68 | 70 | 71 |
Where do you want to go today? 56 | Welcome To Microsoft 60 | 64 |
69 |
72 | 73 |

74 | 75 | 76 | 77 | 79 | 80 | 82 | 83 | 84 | 85 | 131 | 132 | 133 | 134 | 136 | 137 | 138 | 139 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 331 | 332 | 333 | 334 |
78 | 81 | 87 |
88 |
89 | News for You:
90 |
91 | System 92 | Pro
93 |
94 | Industry 95 |
96 |
97 | Channel 98 |
99 |
100 | Small 101 | Business
102 |
103 | Web 104 | Builder
105 |
106 | Developer 107 |
108 |
109 | IT 110 | Executive
111 |
112 | At 113 | Home
114 |
115 | Games 116 |
117 |
118 | Press 119 |
120 |
121 | Shareholder 122 |
123 |
124 | Education 125 |
126 |
127 | Online 128 | Services Provider
129 |

130 |
135 | * 142 | Microsoft 143 | at Comdex: Here's What's Happening
144 |
Some of the highlights:

145 | 146 | 174 | 175 |

* 177 | Microsoft 178 | Announces the Release of Office 97
179 |
Learn about the next version of Microsoft Office, the world's most 180 | popular office suite, and enter a contest to win prizes

181 | 182 |

* 184 | It's 185 | Here! Download Microsoft's Proxy Server
186 |
Find out how easily you can provide secure Internet access to all 187 | desktops in your organization while reducing network congestion, with Microsoft 188 | Proxy Server.

189 | 190 |

* 192 | Microsoft 194 | Partners Announce Support for Windows and BackOffice Logo Programs
195 |
Read press releases and news from companies with these logos now 196 | exhibiting at the Microsoft Partner Pavilion at COMDEX. More than 800 titles 197 | now carry the Designed for Windows 95, BackOffice, or Windows NT and Windows 198 | 95 logos.

199 | 200 |

201 | 202 |

204 | Read More News
205 |
206 |
207 | Solutions
208 |
209 |
210 | 211 | High Tech Tools 212 | Aid Public Safety, but Boston Wants More
213 | How Boston police hook up with high tech: the mayor of Boston and the police 214 | commissioner talk technology.
215 |
216 |
217 |
218 |
219 |
220 | Action Items
221 |
222 |
223 | Learn About Microsoft

224 | 225 |

Download 226 | Free Software

227 | 228 |

Find 229 | an Event

230 | 231 |

Get 232 | Trained/Certified on Microsoft Products
233 |
234 |
235 | Return of Arcade: Pac-man 237 | Last Updated 11/20/96 238 | 1:51 PM
239 |
240 | ©
1996 242 | Microsoft Corporation. All Rights Reserved. 243 |

244 |
245 |

246 |
255 |

Microsoft Internet Explorer 258 |
259 |
260 | Microsoft Software for the Internet 262 |
263 |
264 |
265 | Visit Microsoft's Comdex Site 267 |
268 |
269 |
270 |
271 |
272 | Worlwide Sites
273 |

320 |
321 | 322 |


324 |
325 |
326 | About This Site 329 |

330 |
335 | 336 |

337 |

338 | 339 | 340 | 341 | 343 | 344 |
342 |
345 | 346 |
347 |

348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 |

358 |
359 | 360 |

COMDEX KEYNOTE 361 | SETS '97 VISION 362 |

363 | 364 |

365 |

366 | 367 | 368 | 369 | 440 | 441 | 444 | 445 | 549 | 550 |
371 | NOVEMBER 21: Check out details of CEO Jim Barksdale's 372 | Comdex keynote address on 373 | Netscape's broad line of client-server 374 | solutions and Constellation, 375 | Netscape's new netcasting and desktop-customization technology.
376 | 377 |

379 | See where you'll be in 1997 with Netscape 380 | Communicator: an open email, groupware, and browser suite that provides 381 | the complete set of tools you need every day to communicate, share, and 382 | access information.
383 |

384 | 385 |

386 | Bring real-time, streaming audio and synchronized multimedia to your desktop 387 | or network and download 388 | the latest versions of Netscape Media 389 | Server 1.0 and Netscape Media 390 | Player 1.0 now.
391 |

392 | 393 |

395 | Get up-to-the-minute news about the digital world from Wired and 396 | health reports from leading sources - all broadcast directly to your computer 397 | by the PointCast Network, the free, 398 | award-winning Navigator 400 | plug-in.
401 |

402 | 403 |

405 | Today only: Don't miss the live Macromedia 407 | Shockwave audio feed of funk artist George Clinton from Universal Amphitheatre. 408 | Cosponsored by Capital Records, "Live From the Mothership" marks 409 | Macromedia's first live audio 410 | broadcast using Shockwave technology.

411 | 412 |


413 |

414 | 415 |

MORE NEWS: Netscape 416 | In-Box Direct for SuiteSpot 417 | delivers customized news and analysis to your mailbox ... Join the Netscape 418 | team ... Sun's Scott McNealy 419 | shares his thoughts on Java computing in this week's "Intranet 420 | Executive" column ... Virgin Communications uses 422 | Netscape Navigator for new Internet dial-up service ... Industry leaders 423 | to advance standardization of Netscape's JavaScript 424 | at standards body meeting.

425 | 426 |

427 |


428 |

429 | 430 |

FOCUS: IN 431 | THE ENTERPRISE

432 | 433 |

By rapidly adopting Netscape intranet 434 | solutions, Amdahl realized a return on investment of more than 2000 436 | percent in less than seven weeks, according to a recent study by International 437 | Data Corporation. Download 438 | Netscape Servers, build your own intranet, and start saving now.

439 |
442 |

443 |
DOWNLOAD 446 | OR PURCHASE THE LATEST 447 | NETSCAPE SOFTWARE 448 | 449 |
450 | 451 | 452 | 486 | 487 | 488 | 489 | 490 | 491 |
JavaScript Enabled
492 |
493 | 494 |

496 |

497 | 498 |

499 | 500 | 501 | 502 | 503 | 504 |


505 |

506 | 507 |

GENERAL STORE 508 | SPECIAL

509 | 510 |

Save $42 on Navigator 512 | 3.0 + Subscription + PowerPack. Take note: Save $2 on a two-pack of 513 | Netscape spiral 514 | notebooks.

515 | 516 |

517 |


518 |

519 | 520 |

NETSCAPE COLUMNS 521 |

522 | 523 |

Keep up with current Internet issues and trends: 524 | Read Netscape Columns for first-person 525 | insights from leading executives, designers, technical experts, journalists, 526 | and analysts. Add your comments and thoughts to the discussion in the Netscape 528 | Columns newsgroup.

529 | 530 |

The Main Thing: Just the Beginning
531 |
Jim Barksdale, president and CEO of Netscape Communications, details 532 | Netscape's business 533 | strategy and talks about how Netscape's new products can help make 534 | intranets pay off for companies. (November 20)

535 | 536 |

TechVision: LDAP
537 |
Marc Andreessen, senior VP of technology for Netscape Communications, 538 | talks about the need for universally 539 | accessible directories and discusses why LDAP provides a solution. 540 | (November 20)

541 | 542 |

Intranet Executive: From Spreadsheet to Websheet 543 |
544 |
Scott McNealy, chairman, president, and CEO of Sun Microsystems, discusses 545 | how firms on Wall Street are using intranets and Java 546 | computing to bring real meaning to one of the original killer apps, 547 | the spreadsheet. (November 20)

548 |
551 | 552 |
553 | 554 | 555 | 558 | 559 | 562 | 563 |
Best viewed with Netscape Navigator. Download 556 | Netscape now! 557 | 561 |
564 |
565 | 566 |

567 |


568 |

569 | 570 | 571 | 572 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 |
596 | 597 |

If these features don't seem to be working, you should 598 | download the JavaScript-enabled 599 | Netscape Navigator. 600 |


601 |
602 |

603 | 604 | 605 | 606 | 614 | 615 | 616 | 617 | 637 | 638 | 639 | 640 | 643 | 644 | 645 | 646 | 649 | 650 | 651 | 652 | 670 | 671 | 672 | 673 | 687 | 688 | 689 | 690 | 693 | 694 | 695 | 696 | 699 | 700 | 701 | 702 | 722 | 723 | 724 | 725 | 750 | 751 | 752 | 753 | 756 | 757 | 758 | 759 | 762 | 763 | 764 | 765 | 774 | 775 | 776 | 777 | 794 | 795 | 796 | 797 | 800 | 801 | 802 | 803 | 806 | 807 | 808 | 809 | 822 | 823 | 824 | 825 | 844 | 845 | 846 | 847 | 850 | 851 | 852 | 853 | 856 | 857 | 858 | 859 | 869 | 870 | 871 | 872 | 883 | 884 | 885 | 886 | 889 | 890 | 891 | 892 | 895 | 896 | 897 | 898 | 905 | 906 | 907 | 908 | 915 | 916 | 917 | 918 | 921 | 922 | 923 | 924 | 927 | 928 | 929 | 930 | 939 | 940 | 941 | 942 | 961 | 962 | 963 | 964 | 967 | 968 | 969 | 970 | 973 | 974 | 975 | 976 | 982 | 983 | 984 | 985 | 998 | 999 | 1000 | 1001 | 1004 | 1005 | 1006 | 1007 | 1010 | 1011 | 1012 | 1013 | 1019 | 1020 | 1021 | 1022 | 1031 | 1032 | 1033 | 1034 | 1037 | 1038 | 1039 | 1040 | 1043 | 1044 | 1045 | 1046 | 1068 | 1069 | 1070 | 1071 | 1080 | 1081 |
Download 607 | Software
608 |
Netscape Navigator
609 |
Server & Developer 610 | Software
611 |
Navigator Plug-ins
612 |
Netscape Now Programs
613 |
TOUR THE 618 | BEST NEW FEATURES 619 | OF NAVIGATOR 3.0
620 |
Take a multimedia Guided 621 | Tour of some hot new features in Navigator 623 | 3.0 - the world's most popular Internet client - and pick up a copy 624 | of our new in-depth Reviewer's 625 | Guide to take along with you. 626 |

628 | GET THE LATEST DIRECTORY 629 | SERVER
630 |
Netscape proudly announces the first public beta of Netscape Directory 632 | Server 1.0 for Windows NT. Download 633 | this feature-complete release and try one of the fastest LDAP servers on 634 | the planet. An included synchronization tool allows you to integrate the 635 | Windows NT directory.

636 |
641 |
642 |
647 |
648 |
Netscape Destinations
653 |
Today's News
654 |
Business
655 |
Finance
656 |
Technology News
657 |
Technology Companies
658 |
Travel
659 |
Sports & Fitness
660 |
Entertainment
661 |
Shopping
662 |
Net Search
663 |
People
664 |
Yellow Pages
665 |
What's New?
666 |
What's Cool?
667 |
Newsgroups
668 |
Customer Showcase
669 |
675 | FIND EXCITING SPORTS 676 | WITH DESTINATIONS
677 |
Don't know where to go on the Web? Visit Netscape 678 | Destinations to find exciting sites in 15 categories, including Technology 679 | News, Finance, Travel, Sports, Yellow Pages, and more. 680 |

SEARCH WITH THE BEST 681 |
682 |
Net Search brings together 683 | the hottest search engines on the Web - Excite, Infoseek, Lycos, Magellan, 684 | and Yahoo - and directs you to directories of business information, shareware, 685 | newsgroups, classified ads, Java applets, and more.

686 |
691 |
692 |
697 |
698 |
Company & 703 | Products
704 |
Employment 705 | Opportunities
706 |
1997 Product Strategy
707 |
1997 Technology Preview
708 |
Netscape Navigator
709 |
Netscape Communicator
710 |
Netscape Tools
711 |
Netscape Commerce Products
712 |
Netscape Servers
713 |
Netscape Sales
714 |
Press Releases
715 |
Netscape Columns
716 |
Investor 717 | Relations
718 |
Channel Partners
719 |
Applications & 720 | Solutions
721 |
727 | NETSCAPE OFFERS LINE 728 | OF INTEGRATED SOLUTIONS 729 |
730 |
Netscape announced a 731 | new line of integrated solutions 732 | that will lead the third wave of networking technology, including open 733 | email and groupware. Netscape intranet customers are already realizing 734 | a 1000 percent return on investment, and now Netscape's new intranet solutions 735 | enable corporate customers to build and manage full-service intranets with 736 | an integrated server suite and client software. 737 |

CHECK OUT NETSCAPE PARTNER 738 | PROGRAMS
739 |
Netscape 740 | Partner Programs address the specific needs of resellers, 742 | OEMs, 743 | trainers, 744 | and Netscape developers. 745 | Each offers specialized resources and benefits to help you better serve 746 | your customers and generate new business prospects. Partner Program participants 747 | also enjoy exclusive access to Partner Pavilion, our private Web site for 748 | channel partners.

749 |
754 |
755 |
760 |
761 |
Intranet 766 | Solutions
767 |
Application 768 | Demos
769 |
Customer Profiles
770 |
Intranet White Papers
771 |
Seminars
772 |
Press Clippings
773 |
INTRANET ROIS 778 | OF 1000 PERCENT AND HIGHER 779 |
780 |
International Data Corporation has released results of in-depth return-on-investment 781 | case studies on Netscape intranet customers Booz-Allen & Hamilton, 782 | Cadence Design Systems, and Silicon Graphics. Read 783 | all about IDC's findings and methodology. 784 |

ASK INTRANET STRATEGY 785 | QUESTIONS AT FORUMS 786 |
787 |
In addition to our AppFoundry Applications and Tools forums, Netscape 788 | now offers several forums 789 | monitored by our systems integration and consulting partners: BBN Planet, 790 | Booz-Allen & Hamilton, CSC, and KPMG Peat Marwick LLP. Look to these 791 | forums to discuss methodologies and strategic issues of intranet deployment. 792 |

793 |
798 |
799 |
804 |
805 |
DevEdge 810 | Online
811 |
Registration
812 |
News
813 |
Support
814 |
Library
815 |
Courses 816 | & Conferences
817 |
Marketing 818 | Assistance
819 |
Netscape 820 | ONE
821 |
828 | PROMOTE YOUR EXPERTISE 829 | TO ENTERPRISES
830 |
Netscape AppFoundry 831 | features free, reusable business applications developed by Netscape partners 832 | and used by corporate customers in their intranets. Developers who create 833 | AppFoundry apps receive extensive visibility that leads to new projects. 834 | Netscape is now actively recruiting AppFoundry developers. Learn 836 | how to participate. 837 |

IFC MOVES FROM ALPHA 838 | TO BETA
839 |
Beta 1 of the Internet 840 | Foundation Classes are now publicly available. Four new examples and 841 | more complete documentation are ready to view. IFC Beta 1 represents a 842 | feature-complete version of the IFCs.

843 |
848 |
849 |
854 |
855 |
General 860 | Store
861 |
Netscape 862 | Navigators
863 |
Netscape 864 | Servers
865 |
Publications
866 |
Logo 867 | Products
868 |
PURCHASE NETSCAPE 873 | SERVERS THROUGH THE GENERAL 874 | STORE
875 |
All Netscape Servers may be purchased securely online through the Netscape 876 | General Store. Save $1675 and get five servers for the price of four when 877 | you buy SuiteSpot. 879 | Hedge against the future and get Enterprise 881 | Server + Subscription. 882 |
887 |
888 |
893 |
894 |
Assistance
899 |
Technical Support
900 |
DevEdge Online
901 |
Creating Net Sites
902 |
Technical Education
903 |
User Groups
904 |
NEW ONLINE 909 | TECH SUPPORT
910 |
How can I register my copy of Netscape Navigator? How do I configure 911 | my Proxy Server's firewall? Can I download the FastTrack Server? Get answers 912 | to these and other really hard questions on our new Technical 913 | Support site. 914 |
919 |
920 |
925 |
926 |
ONE Stop Software
931 |
Netscape Download
932 |
Automatic Update
933 |
Netscape 934 | ONE Directory
935 |
AppFoundry
936 |
Featured 937 | Java Applets
938 |
Hot Software Sites
945 | TAP INTO NETSCAPE 946 | ONE RESOURCES
947 |
Connect 948 | instantly to the global community of partners supporting Netscape ONE, 949 | the open network environment. Whether you're looking for the latest tools 950 | in custom development or searching for an innovative third party to help 951 | you implement an intranet or Internet solution, you'll find assistance 952 | here. 953 |

APPFOUNDRY IS 954 | DEVELOPER'S INTRANET SOURCE 955 |
956 |
Visit Netscape AppFoundry 957 | and see why it's the enterprise developer's source for starter applications, 958 | tools, and expert forums for quickly building and deploying open intranet 959 | applications.

960 |
965 |
966 |
971 |
972 |
Netscape 978 | In-Box Direct
979 |
Developer Information
980 |
FAQ
981 |
988 | IN-BOX DIRECT 989 | DELIVERS RICH INTERACTION 990 |
991 |
Visit In-Box 992 | Direct to take advantage of the New 993 | York Times Direct. Also sign up for a variety of other services, including 994 | HotWired, GiftONE, and TechInvestor. Only Netscape and In-Box Direct deliver 995 | the richness and interactivity of the Web right to your in-box. And right 996 | now, all of our content partners are offering their services for free! 997 |
1002 |
1003 |
1008 |
1009 |
PowerStart
1014 |
PowerStart 1015 | Feedback
1016 |
PowerStart 1017 | Q&A
1018 |
BUILD A CUSTOM 1023 | START PAGE FOR THE WEB 1024 |
1025 |
PowerStart 1026 | is a uniquely efficient way to navigate the Internet. A simple setup session 1027 | lets you choose which links, features, and looks you want to include on 1028 | your PowerStart page. After that, each time you go to PowerStart, your 1029 | selections appear on a single page. 1030 |
1035 |
1036 |
1041 |
1042 |
International
1047 |
Brazilian 1048 | Portuguese Site
1049 |
Danish 1050 | Site
1051 |
Dutch 1052 | Site
1053 |
French 1054 | Site
1055 |
German 1056 | Site
1057 |
Italian 1058 | Site
1059 |
Japanese 1060 | Site
1061 |
Korean 1062 | Site
1063 |
Spanish 1064 | Site
1065 |
Swedish 1066 | Site
1067 |
NETSCAPE ANNOUNCES 1072 | ONLINE INTERNATIONAL STORE 1073 |
1074 |
Netscape announces the opening of its International 1075 | General Store, an online store where customers worldwide can purchase 1076 | Netscape software and merchandise. Netscape's International General Store 1077 | is available in English, French, and German, and supports shopping in French 1078 | francs, pounds sterling, German marks and U.S. dollars. 1079 |
1082 | 1083 |

1084 |


1085 |

1086 | 1087 |

1090 |

1091 | 1092 |
1093 |

1094 | 1095 | 1096 | 1097 | 1098 | 1099 | 1100 | 1101 | 1102 | 1103 | Corporate 1104 | Sales: 415/937-2555; Personal Sales: 415/937-3777; Federal Sales: 415/937-3678
1105 | If you have any questions, please visit Customer 1106 | Service.

1107 |
1108 | 1109 |

Copyright © 1996 Netscape 1110 | Communications Corporation

1111 | 1112 | 1113 | 1114 | --------------------------------------------------------------------------------