├── .gitignore
├── 3270-Regular.woff
├── Dockerfile
├── README.md
├── favicon.ico
├── index.html
├── login.html
├── requirements.txt
├── run.sh
├── server.py
└── web3270.ini
/.gitignore:
--------------------------------------------------------------------------------
1 | ca.csr
2 | ca.key
--------------------------------------------------------------------------------
/3270-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MVS-sysgen/web3270/d37998dd84aa545e4ad7b9d1ce567a0a26073b86/3270-Regular.woff
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3
2 | RUN apt-get update && \
3 | apt-get install -y openssl c3270
4 | WORKDIR /web3270
5 | COPY requirements.txt ./
6 | RUN pip install --no-cache-dir --upgrade pip && \
7 | pip install --no-cache-dir -r requirements.txt
8 | RUN openssl req -x509 -nodes -days 365 \
9 | -subj "/C=CA/ST=QC/O=web3270 Inc/CN=3270.web" \
10 | -newkey rsa:2048 -keyout ca.key \
11 | -out ca.csr
12 | ADD run.sh index.html server.py web3270.ini login.html favicon.ico 3270-Regular.woff ./
13 | RUN chmod +x run.sh
14 | VOLUME ["/config","/certs"]
15 | EXPOSE 80 443
16 | ENTRYPOINT ["/web3270/run.sh"]
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # web3270
2 |
3 | A web based front end for c3270. Uses Xterm.js and terminado.
4 |
5 | ## How to Use
6 |
7 | 1) First install `c3270` (on Debian based systems you can install with `sudo apt install c3270`)
8 | 2) Install the required packages: `pip install -r requirements.txt`
9 | 3) Edit the config file `web3270.ini` customized to your system (see below)
10 | 4) Place your certifcate crt/key file in this folder (or any folder see arguments below)
11 | 4) Start the server: `python3 server.py`
12 |
13 |
14 | ## Config File
15 |
16 | The config file allows you to specify multiple options:
17 |
18 | * `server_ip` IP address of the tnN3270 server to connect to
19 | * `server_port` TCP port of the tn3270 server to connect to
20 | * `webport` TCP port for this webserver
21 | * `tls` **yes**/**no** should this server use HTTPS. If yes this script will check in the folder denoted by the script argument `--certs`, the default is the folder used the run the script.
22 | * `encrypted` **yes**/**no** when connecting to tn3270 use SSL or not
23 | * `selfsignedcert` **yes**/**no** allow self signed certs when connecting to encrypted tn3270 server
24 | * `model` tn3270 model type (2 through 5), default 4. See https://x3270.miraheze.org/wiki/3270_models for more details.
25 | * `useproxy` **yes**/**no** should a proxy be used to connect to tn3270 server
26 | * `proxystring` The proxy connection string *optional*.
27 | * `password` If set a login page will be displayed and the password set here will be required to load web3270 *optional*
28 | * `secret` The secret used to generate secure tokens. If not set the script will set a random one for you *optional*
29 |
30 | ## Script Arguments
31 |
32 | * `--config` the folder where `web3270.ini` resides. If this file does not exist in the folder provided a default config will be created.
33 | * `--certs` the folder where the web server TLS certificates reside. Files required are `ca.csr` and `ca.key`, use the commad below to generate self signed certs:
34 |
35 | ```bash
36 | openssl req -x509 -nodes -days 365 \
37 | -subj "/C=CA/ST=QC/O=web3270 Inc/CN=3270.web" \
38 | -newkey rsa:2048 -keyout ca.key \
39 | -out ca.csr
40 | ```
41 |
42 |
43 | ## Docker
44 |
45 | To build a docker container use: `docker build --tag "mainframed767/web3270:latest" .`
46 |
47 | To run the container:
48 |
49 | ```bash
50 | docker run -d \
51 | --name=web3270 \
52 | -p 4443:443 \
53 | -v /opt/docker/web3270:/config \
54 | -v /opt/docker/web3270/certs:/certs \
55 | --restart unless-stopped \
56 | mainframed767/web3270
57 | ```
58 | This command will run web3270 on port 4443.
59 |
60 | After the first run the config file `web3270.ini` will be placed in `/opt/docker/web3270`. Edit that file to fit your environment then restart the container.
61 |
62 | ## Known Bugs
63 |
64 | * Changing the font only shows the current row. Resizing the browser window fixes this.
--------------------------------------------------------------------------------
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MVS-sysgen/web3270/d37998dd84aa545e4ad7b9d1ce567a0a26073b86/favicon.ico
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
34 |
35 |
36 | {{warning}}
37 | This system is protected by a secret code.
38 | Enter it below to gain access.
39 |
42 |
43 |
44 |
By: Soldier of FORTRAN
45 |
46 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | terminado==0.12.1
2 | tornado==6.1
3 |
--------------------------------------------------------------------------------
/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Docker is unable to run python on its own for some reason
3 | # This script is needed to run properly
4 | python3 -u /web3270/server.py --config /config --certs /certs
5 |
--------------------------------------------------------------------------------
/server.py:
--------------------------------------------------------------------------------
1 | import tornado.web
2 | from tornado.ioloop import IOLoop
3 | from terminado import TermSocket, TermManagerBase, UniqueTermManager
4 | import os, sys
5 | import signal
6 | import configparser
7 | import argparse
8 | import shutil
9 | import random
10 | import secrets
11 |
12 | warnings = [
13 | '''
14 | ██ ██ █████ ██████ ███ ██ ██ ███ ██ ██████
15 | ██ ██ ██ ██ ██ ██ ████ ██ ██ ████ ██ ██
16 | ██ █ ██ ███████ ██████ ██ ██ ██ ██ ██ ██ ██ ██ ███
17 | ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
18 | ███ ███ ██ ██ ██ ██ ██ ████ ██ ██ ████ ██████''',
19 | '''
20 | █ █░ ▄▄▄ ██▀███ ███▄ █ ██▓ ███▄ █ ▄████
21 | ▓█░ █ ░█░▒████▄ ▓██ ▒ ██▒ ██ ▀█ █ ▓██▒ ██ ▀█ █ ██▒ ▀█▒
22 | ▒█░ █ ░█ ▒██ ▀█▄ ▓██ ░▄█ ▒▓██ ▀█ ██▒▒██▒▓██ ▀█ ██▒▒██░▄▄▄░
23 | ░█░ █ ░█ ░██▄▄▄▄██ ▒██▀▀█▄ ▓██▒ ▐▌██▒░██░▓██▒ ▐▌██▒░▓█ ██▓
24 | ░░██▒██▓ ▓█ ▓██▒░██▓ ▒██▒▒██░ ▓██░░██░▒██░ ▓██░░▒▓███▀▒
25 | ░ ▓░▒ ▒ ▒▒ ▓▒█░░ ▒▓ ░▒▓░░ ▒░ ▒ ▒ ░▓ ░ ▒░ ▒ ▒ ░▒ ▒
26 | ▒ ░ ░ ▒ ▒▒ ░ ░▒ ░ ▒░░ ░░ ░ ▒░ ▒ ░░ ░░ ░ ▒░ ░ ░
27 | ░ ░ ░ ▒ ░░ ░ ░ ░ ░ ▒ ░ ░ ░ ░ ░ ░ ░
28 | ░ ░ ░ ░ ░ ░ ░ ░
29 | ''',
30 | '''
31 | ██╗ ██╗ █████╗ ██████╗ ███╗ ██╗██╗███╗ ██╗ ██████╗
32 | ██║ ██║██╔══██╗██╔══██╗████╗ ██║██║████╗ ██║██╔════╝
33 | ██║ █╗ ██║███████║██████╔╝██╔██╗ ██║██║██╔██╗ ██║██║ ███╗
34 | ██║███╗██║██╔══██║██╔══██╗██║╚██╗██║██║██║╚██╗██║██║ ██║
35 | ╚███╔███╔╝██║ ██║██║ ██║██║ ╚████║██║██║ ╚████║╚██████╔╝
36 | ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝╚═╝ ╚═══╝ ╚═════╝
37 | '''
38 | ]
39 |
40 | class BaseHandler(tornado.web.RequestHandler):
41 | def get_current_user(self):
42 | return self.get_secure_cookie("user")
43 |
44 | class TerminalPageHandler(BaseHandler):
45 |
46 | def initialize(self, width=80,height=45):
47 | self.width = width
48 | self.height = height
49 |
50 | def get(self):
51 | print("[+] User logging in:", self.current_user)
52 | if PASSWORD and not self.current_user:
53 | self.redirect("/login")
54 | return
55 |
56 | return self.render(
57 | "index.html",
58 | width=self.width,
59 | height=self.height,
60 | ws_url_path="/websocket"
61 | )
62 |
63 | class LoginHandler(BaseHandler):
64 | def get(self):
65 | # self.write('
')
69 | self.render('login.html',warning=warnings[random.randint(0, 2)])
70 |
71 | def post(self):
72 | if self.get_argument("passed") == PASSWORD:
73 | self.set_secure_cookie("user", secrets.token_urlsafe())
74 | self.redirect("/")
75 | else:
76 | self.redirect("/login")
77 |
78 | class Unique3270Manager(TermManagerBase):
79 | """Give each websocket a unique terminal to use."""
80 |
81 | def __init__(self, max_terminals=None, theight=45,twidth=80, **kwargs):
82 | super(Unique3270Manager, self).__init__(**kwargs)
83 | self.max_terminals = max_terminals
84 | self.height = theight
85 | self.width = twidth
86 |
87 | def get_terminal(self, url_component=None):
88 | if self.max_terminals and len(self.ptys_by_fd) >= self.max_terminals:
89 | raise MaxTerminalsReached(self.max_terminals)
90 |
91 | term = self.new_terminal(height=self.height, width=self.width)
92 | self.start_reading(term)
93 | return term
94 |
95 | def client_disconnected(self, websocket):
96 | """Send terminal SIGHUP when client disconnects."""
97 | self.log.info("Websocket closed, sending SIGHUP to terminal.")
98 | if websocket.terminal:
99 | if os.name == 'nt':
100 | websocket.terminal.kill()
101 | # Immediately call the pty reader to process
102 | # the eof and free up space
103 | self.pty_read(websocket.terminal.ptyproc.fd)
104 | return
105 | websocket.terminal.killpg(signal.SIGHUP)
106 |
107 |
108 | parser = argparse.ArgumentParser(description='web3270 - Web based front end to c3270')
109 | parser.add_argument('--config',help='web3270 Config folder',default=os.path.dirname(os.path.realpath(__file__)))
110 | parser.add_argument('--certs',help='web3270 TLS Certificates folder',default=os.path.dirname(os.path.realpath(__file__)))
111 | args = parser.parse_args()
112 | if not os.path.exists("{}/web3270.ini".format(args.config)):
113 | print("[+] {}/web3270.ini does not exist, copying".format(args.config))
114 | shutil.copy2("{}/web3270.ini".format(os.path.dirname(os.path.realpath(__file__))), args.config)
115 |
116 | print("[+] Using config: {}/web3270.ini".format(args.config))
117 | config = configparser.ConfigParser(comment_prefixes = '/', allow_no_value = True)
118 | config.read("{}/web3270.ini".format(args.config))
119 | PASSWORD = None
120 |
121 | if __name__ == '__main__':
122 | print("[+] Starting Web server")
123 | # defaults
124 | height = 45
125 | width = 80
126 | c3270 = ['c3270', '-secure', '-defaultfgbg']
127 |
128 | if not config['web']['secret']:
129 | config['web']['secret'] = secrets.token_urlsafe()
130 | error = "[+] 'secret =' in {}/web3270.ini is blank. Setting it to: {}".format(args.config, config['web']['secret'])
131 | print(error)
132 | with open("{}/web3270.ini".format(args.config), 'w') as configfile:
133 | config.write(configfile)
134 |
135 | if config['tn3270'].getboolean('selfsignedcert'):
136 | c3270.append('-noverifycert')
137 |
138 | if config['tn3270'].getboolean('useproxy'):
139 | c3270.append('-proxy')
140 | c3270.append(config['proxystring'])
141 | # build connection string
142 |
143 | c3270.append("-model")
144 | c3270.append(config['tn3270']['model'])
145 |
146 | if config['tn3270']['model'] == 2:
147 | height = 24 + 2
148 | elif config['tn3270']['model'] == 3:
149 | height = 32 + 2
150 | elif config['tn3270']['model'] == 4:
151 | height = 43 + 2
152 | elif config['tn3270']['model'] == 5:
153 | height = 27 + 2
154 | width = 132
155 |
156 | connect_string = ""
157 |
158 | if config['tn3270'].getboolean('encrypted'):
159 | connect_string = "L:"
160 |
161 | connect_string += config['tn3270']['server_ip'] + ":" + config['tn3270']['server_port']
162 |
163 | c3270.append(connect_string)
164 |
165 | #c3270 = ['c3270', '-secure', '-noverifycert','L:192.168.0.102:2323']
166 | print("[+] c3270 connect string: '{}'".format(' '.join(c3270)))
167 |
168 |
169 | term_manager = Unique3270Manager(theight=height,twidth=width,shell_command=c3270)
170 | handlers = [
171 | (r"/websocket", TermSocket, {'term_manager': term_manager}),
172 | (r"/", TerminalPageHandler)
173 | ]
174 | # if the password field in the ini file is uncommented add the login handler
175 | if 'password' in config['web']:
176 | PASSWORD = config['web']['password']
177 | print("[+] Password set to {}".format(PASSWORD))
178 | handlers.append((r"/login", LoginHandler))
179 |
180 | handlers.append((r"/(.*)", tornado.web.StaticFileHandler, {'path':'.'}))
181 | app = tornado.web.Application(handlers, cookie_secret=config['web']['secret'])
182 | if config['web'].getboolean('tls'):
183 | csr = "{}/ca.csr".format(args.certs)
184 | key = "{}/ca.key".format(args.certs)
185 | if not os.path.exists(csr):
186 | print("[!] Could not find {} trying local cert ca.csr".format(csr))
187 | csr = "{}/ca.csr".format(os.path.dirname(os.path.realpath(__file__)))
188 | if not os.path.exists(csr):
189 | print("[!] Could not find {}".format(csr))
190 | sys.exit(-1)
191 | if not os.path.exists(key):
192 | print("[!] Could not find {} trying local cert ca.key".format(key))
193 | key = "{}/ca.key".format(os.path.dirname(os.path.realpath(__file__)))
194 | if not os.path.exists(key):
195 | print("[!] Could not find {}".format(key))
196 | sys.exit(-1)
197 |
198 | print("[+] Using cert files {} and {}".format(csr,key))
199 |
200 | http_server = tornado.httpserver.HTTPServer(app, ssl_options={
201 | "certfile": csr,
202 | "keyfile": key,
203 | })
204 | http_server.listen(config['web']['webport'])
205 | print("[+] Secure web server Listening on port {}".format(config['web']['webport']))
206 | else:
207 | app.listen(config['web']['webport'])
208 | print("[+] Web server Listening on port {}".format(config['web']['webport']))
209 | IOLoop.current().start()
--------------------------------------------------------------------------------
/web3270.ini:
--------------------------------------------------------------------------------
1 | [tn3270]
2 | server_ip = 192.168.0.102
3 | server_port = 2323
4 | ; should the connection be encrypted
5 | encrypted = yes
6 | ; enables -noverifycert
7 | selfsignedcert = yes
8 | ; tn3270 model type
9 | model = 4
10 | ; to use proxies change below to yes and uncomment the proxystring
11 | ; and adjust as needed
12 | useproxy = no
13 | ;proxystring = socks5d:fred:secret@localhost:12345
14 |
15 | [web]
16 | webport = 443
17 | tls = yes
18 | ; use this to set a password required to access the web app
19 | ; if this line is uncommented no password is required
20 | ;password =
21 | ; This is used for secure cookies. If you do not set one
22 | ; this script will set one for you
23 | secret =
--------------------------------------------------------------------------------