Created
December 14, 2018 13:35
-
-
Save pdparchitect/a4840d25a88f71d8a051631c54795b11 to your computer and use it in GitHub Desktop.
Basic Proxy TUI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const os = require('os') | |
const fs = require('fs') | |
const path = require('path') | |
const getDefaults = (name) => { | |
const filepath = path.join(os.homedir(), '.pown', name + '.json') | |
if (fs.existsSync(filepath)) { | |
return JSON.parse(fs.readFileSync(filepath)) | |
} else { | |
return {} | |
} | |
} | |
exports.getDefaults = getDefaults |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const debounce = require('debounce') | |
const { screen, Question } = require('neo-blessed') | |
const Table = require('./table') | |
const Request = require('./request') | |
const Response = require('./response') | |
const { getDefaults } = require('./defaults') | |
const { tui: tuiDefaults = {} } = getDefaults('proxy') | |
const sc = screen({ | |
title: 'Proxy' | |
}) | |
sc.key(['tab'], function (ch, key) { | |
sc.focusNext() | |
}) | |
sc.key(['q', 'C-c', 'C-x'], function (ch, key) { | |
const question = new Question({ | |
keys: true, | |
top: 'center', | |
left: 'center', | |
width: '50%', | |
height: 5, | |
border: { | |
type: 'line' | |
}, | |
style: { | |
border: { | |
fg: 'grey' | |
} | |
} | |
}) | |
sc.append(question) | |
question.ask('Do you really want to quit?', (err, result) => { | |
if (err) { | |
return | |
} | |
if (result) { | |
return process.exit(0) | |
} | |
sc.remove(question) | |
sc.render() | |
}) | |
}) | |
const render = debounce(() => { | |
sc.render() | |
}, 1000) | |
const transactions = new Table({ | |
...tuiDefaults.transactions, | |
top: 0, | |
left: 0, | |
width: '100%', | |
height: '50%', | |
border: { | |
type: 'line' | |
}, | |
style: { | |
border: { | |
fg: 'grey' | |
} | |
}, | |
columns: [ | |
{ field: 'id', name: '#', width: 13 }, | |
{ field: 'method', name: 'method', width: 7 }, | |
{ field: 'scheme', name: 'scheme', width: 7 }, | |
{ field: 'host', name: 'host', width: 13 }, | |
{ field: 'port', name: 'port', width: 5 }, | |
{ field: 'path', name: 'path', width: 42 }, | |
{ field: 'query', name: 'query', width: 42 }, | |
{ field: 'responseCode', name: 'code', width: 7 }, | |
{ field: 'responseType', name: 'type', width: 13 }, | |
{ field: 'responseLength', name: 'length', width: 21 } | |
], | |
columnSpacing: 3 | |
}) | |
const request = new Request({ | |
...tuiDefaults.request, | |
bottom: 0, | |
left: 0, | |
width: '50%', | |
height: '50%', | |
border: { | |
type: 'line' | |
}, | |
style: { | |
border: { | |
fg: 'grey' | |
} | |
} | |
}) | |
const response = new Response({ | |
...tuiDefaults.response, | |
bottom: 0, | |
right: 0, | |
width: '50%', | |
height: '50%', | |
border: { | |
type: 'line' | |
}, | |
style: { | |
border: { | |
fg: 'grey' | |
} | |
} | |
}) | |
sc.append(transactions) | |
sc.append(request) | |
sc.append(response) | |
transactions.on('select', (a) => { | |
request.display(a) | |
response.display(a) | |
sc.render() | |
}) | |
transactions.focus() | |
let i = 0 | |
setInterval(() => { | |
transactions.addItem({ | |
id: i++, | |
method: 'GET', | |
scheme: 'http', | |
host: 'google.com', | |
port: 80, | |
path: '/' + Math.random(), | |
query: '', | |
responseCode: 200, | |
responseType: 'html', | |
responseLength: 1234 | |
}) | |
render() | |
}, 1000) | |
sc.render() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { Box } = require('neo-blessed') | |
const EMPTY = Buffer.from('') | |
class Request extends Box { | |
constructor (options) { | |
options = { | |
tags: true, | |
scrollable: true, | |
methodColors: { | |
'GET': 'yellow', | |
'POST': 'yellow', | |
'HEAD': 'yellow', | |
'PUT': 'purple', | |
'PATCH': 'red', | |
'DELETE': 'red' | |
}, | |
...options | |
} | |
super(options) | |
} | |
display (request) { | |
const { method, scheme, host, port, path, query, version = 'HTTP/1.1', headers = {}, body = EMPTY } = request | |
const methodColor = this.options.methodColors[method] || 'white' | |
let addressBlock | |
if ((scheme === 'http' && port === 80) || (scheme === 'https' && port === 443)) { | |
addressBlock = `${host}` | |
} else { | |
addressBlock = `${host}:${port}` | |
} | |
const headersBlock = Object.entries(headers).map(([name, value]) => { | |
if (!Array.isArray(value)) { | |
value = [value] | |
} | |
return value.map((value) => { | |
return `{purple-fg}${name}:{/pruple-fg} ${value}` | |
}).join('\n') | |
}).join('\n') | |
const bodyBlock = body.toString() | |
this.setContent(`{${methodColor}-fg}${method}{/${methodColor}-fg} ${scheme}://${addressBlock}${path}${query ? '?' + query : ''} ${version}\n${headersBlock}\n${bodyBlock}`) | |
} | |
} | |
module.exports = Request |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { Box } = require('neo-blessed') | |
const EMPTY = Buffer.from('') | |
class Response extends Box { | |
constructor (options) { | |
options = { | |
tags: true, | |
scrollable: true, | |
codeColors: { | |
'1xx': 'cyan', | |
'2xx': 'green', | |
'3xx': 'yellow', | |
'4xx': 'purple', | |
'5xx': 'red' | |
}, | |
...options | |
} | |
super(options) | |
} | |
display (response) { | |
const { responseCode, responseMessage, responseVersion = 'HTTP/1.1', responseHeaders = {}, responseBody = EMPTY } = response | |
const codeColor = this.options.codeColors[responseCode.toString().replace(/(\d).*/, '$1xx')] || 'white' | |
const headersBlock = Object.entries(responseHeaders).map(([name, value]) => { | |
if (!Array.isArray(value)) { | |
value = [value] | |
} | |
return value.map((value) => { | |
return `{purple-fg}${name}:{/pruple-fg} ${value}` | |
}).join('\n') | |
}).join('\n') | |
const bodyBlock = responseBody.toString() | |
this.setContent(`{${codeColor}-fg}${responseCode}{/${codeColor}-fg} ${responseMessage} ${responseVersion}\n${headersBlock}\n${bodyBlock}`) | |
} | |
} | |
module.exports = Response |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const stripAnsi = require('strip-ansi') | |
const { Box, List } = require('neo-blessed') | |
class Table extends Box { | |
constructor (options) { | |
const { style = {} } = options | |
const { columns: columnsStyle = {}, rows: rowsStyle = {} } = style | |
const { selected: rowsSelectedStyle = {}, item: rowsItemStyle } = rowsStyle | |
options = { | |
columnSpacing: 10, | |
columns: [], | |
items: [], | |
keys: true, | |
vi: false, | |
interactive: true, | |
...options, | |
style: { | |
...style, | |
columns: { | |
bold: true, | |
...columnsStyle | |
}, | |
rows: { | |
selected: { | |
fg: 'white', | |
bg: 'blue', | |
...rowsSelectedStyle | |
}, | |
item: { | |
fg: 'white', | |
bg: '', | |
...rowsItemStyle | |
} | |
} | |
} | |
} | |
super(options) | |
this.columns = new Box({ | |
screen: this.screen, | |
parent: this, | |
top: 0, | |
left: 0, | |
height: 1, | |
width: 'shrink', | |
ailgn: 'left', | |
style: this.options.style.columns | |
}) | |
this.rows = new List({ | |
screen: this.screen, | |
parent: this, | |
top: 2, | |
left: 0, | |
width: '100%', | |
align: 'left', | |
style: this.options.style.rows, | |
keys: options.keys, | |
vi: options.vi, | |
interactive: options.interactive | |
}) | |
this.append(this.columns) | |
this.append(this.rows) | |
this.on('attach', () => { | |
this.setColumns(options.columns) | |
this.setItems(options.items) | |
}) | |
this.rows.on('select', (_, index) => { | |
this.emit('select', this.items[index], index) | |
}) | |
} | |
focus () { | |
this.rows.focus() | |
} | |
render () { | |
if (this.screen.focused === this.rows) { | |
this.rows.focus() | |
} | |
this.rows.width = this.width - 3 | |
this.rows.height = this.height - 4 | |
super.render() | |
} | |
fieldsToContent (fields) { | |
let str = '' | |
fields.forEach((field, index) => { | |
const size = this.columnWidths[index] | |
const strip = stripAnsi(field.toString()) | |
const len = field.toString().length - strip.length | |
field = field.toString().substring(0, size + len) | |
// compensate for len | |
let spaceLength = size - strip.length + this.options.columnSpacing | |
if (spaceLength < 0) { | |
spaceLength = 0 | |
} | |
const spaces = new Array(spaceLength).join(' ') | |
str += field + spaces | |
}) | |
return str | |
} | |
dataToContentItem (d) { | |
return this.fieldsToContent(this.columnFields.map((f) => d[f])) | |
} | |
setColumns (columns) { | |
this.columnFields = [] | |
this.columnNames = [] | |
this.columnWidths = [] | |
columns.forEach((column) => { | |
const { field, name, width } = column | |
this.columnFields.push(field) | |
this.columnNames.push(name) | |
this.columnWidths.push(width) | |
}) | |
this.columns.setContent(this.fieldsToContent(this.columnNames)) | |
} | |
setItems (items) { | |
this.items = [...items] | |
this.rows.setItems(items.map((item) => this.dataToContentItem(item))) | |
} | |
addItem (item) { | |
this.items.push(item) | |
this.rows.addItem(this.dataToContentItem(item)) | |
} | |
} | |
module.exports = Table |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment