├── .gitignore ├── README.md ├── luvit-loader.lua ├── main.lua └── package.lua /.gitignore: -------------------------------------------------------------------------------- 1 | deps 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wscat 2 | 3 | A netcat like client for websockets 4 | 5 | ## Install 6 | 7 | Install using lit. 8 | 9 | ```sh 10 | lit make lit://creationix/wscat 11 | ``` 12 | 13 | And copy the resulting `wscat` binary to somewhere on your path. 14 | 15 | ## Usage 16 | 17 | Usage is url and then optionally subprotocol. 18 | 19 | ```sh 20 | wscat ws://localhost:8080 schema-rpc 21 | ``` 22 | -------------------------------------------------------------------------------- /luvit-loader.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Copyright 2014-2016 The Luvit Authors. All Rights Reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS-IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | ]] 18 | 19 | local hasLuvi, luvi = pcall(require, 'luvi') 20 | local uv, bundle 21 | 22 | if hasLuvi then 23 | uv = require('uv') 24 | bundle = luvi.bundle 25 | else 26 | uv = require('luv') 27 | end 28 | 29 | local getenv = require('os').getenv 30 | 31 | local isWindows 32 | if _G.jit then 33 | isWindows = _G.jit.os == "Windows" 34 | else 35 | isWindows = not not package.path:match("\\") 36 | end 37 | 38 | local tmpBase = isWindows and (getenv("TMP") or uv.cwd()) or 39 | (getenv("TMPDIR") or '/tmp') 40 | local binExt = isWindows and ".dll" or ".so" 41 | 42 | local getPrefix, splitPath, joinParts 43 | if isWindows then 44 | -- Windows aware path utilities 45 | function getPrefix(path) 46 | return path:match("^%a:\\") or 47 | path:match("^/") or 48 | path:match("^\\+") 49 | end 50 | function splitPath(path) 51 | local parts = {} 52 | for part in string.gmatch(path, '([^/\\]+)') do 53 | table.insert(parts, part) 54 | end 55 | return parts 56 | end 57 | function joinParts(prefix, parts, i, j) 58 | if not prefix then 59 | return table.concat(parts, '/', i, j) 60 | elseif prefix ~= '/' then 61 | return prefix .. table.concat(parts, '\\', i, j) 62 | else 63 | return prefix .. table.concat(parts, '/', i, j) 64 | end 65 | end 66 | else 67 | -- Simple optimized versions for UNIX systems 68 | function getPrefix(path) 69 | return path:match("^/") 70 | end 71 | function splitPath(path) 72 | local parts = {} 73 | for part in string.gmatch(path, '([^/]+)') do 74 | table.insert(parts, part) 75 | end 76 | return parts 77 | end 78 | function joinParts(prefix, parts, i, j) 79 | if prefix then 80 | return prefix .. table.concat(parts, '/', i, j) 81 | end 82 | return table.concat(parts, '/', i, j) 83 | end 84 | end 85 | 86 | local function pathJoin(...) 87 | local inputs = {...} 88 | local l = #inputs 89 | 90 | -- Find the last segment that is an absolute path 91 | -- Or if all are relative, prefix will be nil 92 | local i = l 93 | local prefix 94 | while true do 95 | prefix = getPrefix(inputs[i]) 96 | if prefix or i <= 1 then break end 97 | i = i - 1 98 | end 99 | 100 | -- If there was one, remove its prefix from its segment 101 | if prefix then 102 | inputs[i] = inputs[i]:sub(#prefix) 103 | end 104 | 105 | -- Split all the paths segments into one large list 106 | local parts = {} 107 | while i <= l do 108 | local sub = splitPath(inputs[i]) 109 | for j = 1, #sub do 110 | parts[#parts + 1] = sub[j] 111 | end 112 | i = i + 1 113 | end 114 | 115 | -- Evaluate special segments in reverse order. 116 | local skip = 0 117 | local reversed = {} 118 | for idx = #parts, 1, -1 do 119 | local part = parts[idx] 120 | if part ~= '.' then 121 | if part == '..' then 122 | skip = skip + 1 123 | elseif skip > 0 then 124 | skip = skip - 1 125 | else 126 | reversed[#reversed + 1] = part 127 | end 128 | end 129 | end 130 | 131 | -- Reverse the list again to get the correct order 132 | parts = reversed 133 | for idx = 1, #parts / 2 do 134 | local j = #parts - idx + 1 135 | parts[idx], parts[j] = parts[j], parts[idx] 136 | end 137 | 138 | local path = joinParts(prefix, parts) 139 | return path 140 | end 141 | 142 | local function loader(dir, path, bundleOnly) 143 | local errors = {} 144 | local fullPath 145 | local useBundle = bundleOnly 146 | local function try(tryPath) 147 | local prefix = useBundle and "bundle:" or "" 148 | local fileStat = useBundle and bundle.stat or uv.fs_stat 149 | 150 | local newPath = tryPath 151 | local stat = fileStat(newPath) 152 | if stat and stat.type == "file" then 153 | fullPath = newPath 154 | return true 155 | end 156 | errors[#errors + 1] = "\n\tno file '" .. prefix .. newPath .. "'" 157 | 158 | newPath = tryPath .. ".lua" 159 | stat = fileStat(newPath) 160 | if stat and stat.type == "file" then 161 | fullPath = newPath 162 | return true 163 | end 164 | errors[#errors + 1] = "\n\tno file '" .. prefix .. newPath .. "'" 165 | 166 | newPath = pathJoin(tryPath, "init.lua") 167 | stat = fileStat(newPath) 168 | if stat and stat.type == "file" then 169 | fullPath = newPath 170 | return true 171 | end 172 | errors[#errors + 1] = "\n\tno file '" .. prefix .. newPath .. "'" 173 | 174 | end 175 | if string.sub(path, 1, 1) == "." then 176 | -- Relative require 177 | if not try(pathJoin(dir, path)) then 178 | return table.concat(errors) 179 | end 180 | else 181 | while true do 182 | if try(pathJoin(dir, "deps", path)) or 183 | try(pathJoin(dir, "libs", path)) then 184 | break 185 | end 186 | if #dir < 2 then 187 | return table.concat(errors) 188 | end 189 | dir = pathJoin(dir, "..") 190 | end 191 | -- Module require 192 | end 193 | if useBundle then 194 | local key = "bundle:" .. fullPath 195 | return function () 196 | if package.loaded[key] then 197 | return package.loaded[key] 198 | end 199 | local code = bundle.readfile(fullPath) 200 | local module = loadstring(code, key)() 201 | package.loaded[key] = module 202 | return module 203 | end, key 204 | end 205 | fullPath = uv.fs_realpath(fullPath) 206 | return function () 207 | if package.loaded[fullPath] then 208 | return package.loaded[fullPath] 209 | end 210 | local module = loadfile(fullPath)() 211 | package.loaded[fullPath] = module 212 | return module 213 | end 214 | end 215 | 216 | -- Register as a normal lua package loader. 217 | local cwd = uv.cwd() 218 | table.insert(package.loaders, 1, function (path) 219 | 220 | -- Ignore built-in libraries with this loader. 221 | if path:match("^[a-z]+$") and package.preload[path] then 222 | return 223 | end 224 | 225 | local caller = debug.getinfo(3, "S").source 226 | if string.sub(caller, 1, 1) == "@" then 227 | return loader(pathJoin(cwd, caller:sub(2), ".."), path) 228 | elseif string.sub(caller, 1, 7) == "bundle:" then 229 | return loader(pathJoin(caller:sub(8), ".."), path, true) 230 | end 231 | end) 232 | -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | local bundle = require('luvi').bundle 2 | loadstring(bundle.readfile("luvit-loader.lua"), "bundle:luvit-loader.lua")() 3 | 4 | local getenv = require('os').getenv 5 | local isWindows = require('ffi').os == "Windows" 6 | local uv = require('uv') 7 | local fs = require('coro-fs') 8 | local parseUrl = require('coro-websocket').parseUrl 9 | local connect = require('coro-websocket').connect 10 | local readline = require('readline') 11 | local History = readline.History 12 | local Editor = readline.Editor 13 | local split = require('coro-split') 14 | local pp = require('pretty-print') 15 | local p = pp.prettyPrint 16 | local meta = require('./package') 17 | 18 | local options, url 19 | do 20 | local args = {...} 21 | url = args[1] 22 | if not url then 23 | print(string.format("%s v%s", meta.name, meta.version)) 24 | print("usage: wscat url [subprotocol]") 25 | print("example: wscat wss://lit.luvit.io/ lit") 26 | return 1 27 | end 28 | local err 29 | options, err = parseUrl(url) 30 | if err then 31 | print(err) 32 | return -1 33 | end 34 | if options.pathname == "" then 35 | options.pathname = "/" 36 | end 37 | options.subprotocol = args[2] 38 | end 39 | 40 | local getLine, history 41 | do 42 | local thread, editor 43 | local prompt = "" 44 | history = History.new() 45 | editor = Editor.new({ 46 | stdin = pp.stdin, 47 | stdout = pp.stdout, 48 | history = history 49 | }) 50 | 51 | local function onLine(err, line, reason) 52 | local t = thread 53 | thread = nil 54 | return assert(coroutine.resume(t, line, err or reason)) 55 | end 56 | 57 | function getLine() 58 | thread = coroutine.running() 59 | editor:readLine(prompt, onLine) 60 | return coroutine.yield() 61 | end 62 | end 63 | 64 | 65 | coroutine.wrap(function () 66 | 67 | local historyFile 68 | if isWindows then 69 | historyFile = getenv("APPDATA") .. "\\wscat_history" 70 | else 71 | historyFile = getenv("HOME") .. "/.wscat_history" 72 | end 73 | history:load(fs.readFile(historyFile) or "") 74 | 75 | local connectMessage = "Conecting to " .. url 76 | if options.subprotocol then 77 | connectMessage = connectMessage .. " using " .. options.subprotocol 78 | end 79 | print(connectMessage) 80 | local res, read, write = assert(connect(options)) 81 | local peer = res.socket:getpeername() 82 | res.socket:keepalive(true, 1000) 83 | 84 | print(string.format("Connected to %s:%s", peer.ip, peer.port)) 85 | print("(Use Control+D to send EOF)") 86 | 87 | local done = false 88 | local function getInput() 89 | for line in getLine do 90 | assert(write { 91 | opcode = 1, 92 | payload = line 93 | }) 94 | fs.writeFile(historyFile, history:dump()) 95 | end 96 | write() 97 | done = true 98 | end 99 | 100 | local function logMessages() 101 | for message in read do 102 | if message.opcode == 1 then -- text 103 | print(message.payload) 104 | elseif message.opcode == 2 then -- binary 105 | p(message.payload) 106 | end 107 | end 108 | if not done then 109 | print("Server disconnected") 110 | os.exit(-1) 111 | end 112 | end 113 | 114 | split(getInput, logMessages) 115 | 116 | end)() 117 | 118 | uv.run() 119 | -------------------------------------------------------------------------------- /package.lua: -------------------------------------------------------------------------------- 1 | return { 2 | name = "creationix/wscat", 3 | version = "0.2.3", 4 | description = "A netcat like client for websockets", 5 | luvi = { 6 | version = "2.6.1", 7 | flavor = "regular", 8 | }, 9 | homepage = "https://github.com/creationix/wscat", 10 | dependencies = { 11 | "luvit/pretty-print@2.0.0", 12 | "luvit/readline@2.0.0", 13 | "luvit/secure-socket@1.0.0", 14 | "creationix/coro-split@2.0.0", 15 | "creationix/coro-websocket@1.0.0", 16 | "creationix/coro-fs@2.2.1", 17 | }, 18 | files = { 19 | "*.lua", 20 | } 21 | } 22 | --------------------------------------------------------------------------------