Skip to content

Instantly share code, notes, and snippets.

@pdparchitect
Created December 14, 2018 13:35
Show Gist options
  • Save pdparchitect/a4840d25a88f71d8a051631c54795b11 to your computer and use it in GitHub Desktop.
Save pdparchitect/a4840d25a88f71d8a051631c54795b11 to your computer and use it in GitHub Desktop.
Basic Proxy TUI
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
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()
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
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
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