File transfer between a Windows 98 QEMU guest and a macOS host using a lightweight Python HTTP server. This approach was necessary because:
- QEMU's built-in SMB requires
/usr/sbin/smbdwhich is SIP-protected on macOS - Homebrew Samba installs as
samba-dot-org-smbd, notsmbd - macOS File Sharing uses SMBv2/3 — Win98 only speaks SMBv1
- FTP passive mode fails under QEMU SLIRP NAT (guest appears as
127.0.0.1) - IE requires a dial-up modem for internet but works fine with LAN IPs over HTTP
| Address | Role |
|---|---|
10.0.2.2 |
macOS host (gateway) |
10.0.2.3 |
DNS server |
10.0.2.4 |
QEMU built-in SMB (unused) |
10.0.2.15 |
Win98 guest |
mkdir -p ~/sharedThe following line must be present in the Qemu invocation to give the guest a network card:
-netdev user,id=net0 -device pcnet,rombar=0,netdev=net0 \No extra options are needed — the plain default is sufficient for HTTP file sharing.
Open Internet Explorer → View → Internet Options → Connections → select "Never dial a connection" → OK.
This allows IE to use the PCnet LAN adapter without requiring a dial-up modem.
Run this on macOS before or after starting the VM:
python3 -c "
import http.server, os, mimetypes
SHARE = os.path.expanduser('~/shared')
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
# Strip leading slash to get relative path
rel = self.path.lstrip('/')
fpath = os.path.realpath(os.path.join(SHARE, rel))
# Security: prevent path traversal outside SHARE
if not fpath.startswith(os.path.realpath(SHARE)):
self.send_response(403)
self.end_headers()
return
# Serve file download
if os.path.isfile(fpath):
mime, _ = mimetypes.guess_type(fpath)
self.send_response(200)
self.send_header('Content-Type', mime or 'application/octet-stream')
self.send_header('Content-Disposition', 'attachment; filename=\"' + os.path.basename(fpath) + '\"')
self.send_header('Content-Length', str(os.path.getsize(fpath)))
self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
self.send_header('Pragma', 'no-cache')
self.send_header('Expires', '0')
self.end_headers()
with open(fpath, 'rb') as f:
self.wfile.write(f.read())
return
# Serve directory listing
if os.path.isdir(fpath):
entries = sorted(os.listdir(fpath))
items = ''
# Back link if not at root
if rel:
parent = '/'.join(rel.rstrip('/').split('/')[:-1])
items += '<li><a href=\"/' + parent + '\">[..]</a></li>'
for e in entries:
epath = os.path.join(fpath, e)
href = '/' + (rel + '/' + e).lstrip('/')
label = e + '/' if os.path.isdir(epath) else e
items += '<li><a href=\"' + href + '\">' + label + '</a></li>'
upload_path = '/' + rel
page = ('<html><body>'
'<h2>Browse: /' + rel + '</h2>'
'<ul>' + items + '</ul><hr>'
'<h2>Upload here</h2>'
'<form method=POST action=\"' + upload_path + '\" enctype=multipart/form-data>'
'<input type=file name=f>'
'<input type=submit value=Upload>'
'</form></body></html>')
self.send_response(200)
self.end_headers()
self.wfile.write(page.encode())
return
self.send_response(404)
self.end_headers()
def do_POST(self):
rel = self.path.lstrip('/')
dest_dir = os.path.realpath(os.path.join(SHARE, rel))
if not dest_dir.startswith(os.path.realpath(SHARE)):
self.send_response(403)
self.end_headers()
return
ct = self.headers.get('Content-Type', '')
boundary = ct.split('boundary=')[1].encode()
length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(length)
parts = body.split(b'--' + boundary)
for part in parts:
if b'filename=' in part:
fname = part.split(b'filename=\"')[1].split(b'\"')[0].decode()
# Win98 sends full DOS path e.g. C:\WINDOWS\Desktop\file.txt
fname = fname.replace('\\\\', '/').split('/')[-1]
content = part.split(b'\r\n\r\n', 1)[1].rsplit(b'\r\n', 1)[0]
with open(os.path.join(dest_dir, fname), 'wb') as f:
f.write(content)
self.send_response(200)
self.end_headers()
back = '/' + rel
self.wfile.write(('<html><body>Done! <a href=\"' + back + '\">Back</a></body></html>').encode())
def log_message(self, format, *args):
print(format % args)
http.server.HTTPServer(('0.0.0.0', 8080), Handler).serve_forever()
"Requires Python 3.12 or earlier for
cgimodule. On Python 3.13+ thecgimodule was removed — the server above uses raw multipart parsing instead and works on all versions.
Open Internet Explorer and navigate to:
http://10.0.2.2:8080
- Download: click any filename in the list
- Upload: use the file picker at the bottom and click Upload
- The shared folder on macOS is
~/shared - Files uploaded from Win98 appear immediately in that folder
- To share a file with Win98, just copy it into
~/sharedon macOS - The server has no authentication — only accessible from inside the QEMU guest