├── .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 | 5 | 6 | web3270 7 | 31 | 35 | 36 | 37 |
38 | web3270    39 | 40 | Font Size: 49 |     50 | status: 51 | connecting... 52 | 53 |
54 | 55 |
56 |
By: Soldier of FORTRAN
57 | 58 | 59 | 60 | 125 | 126 | -------------------------------------------------------------------------------- /login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | web3270 7 | 31 | 32 | 33 |
34 | 35 |
36 | {{warning}}
37 |     This system is protected by a secret code.

38 |     Enter it below to gain access.

39 |
40 |     Secret code:    41 |
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('
' 66 | # 'Secret code: ' 67 | # '' 68 | # '
') 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 = --------------------------------------------------------------------------------