├── LICENSE ├── README.md ├── socks5-dev-1.rockspec └── socks5.lua /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Boris Nagaev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | lua-resty-socks5 2 | ================ 3 | 4 | Lua SOCKS5 client for the `ngx_lua` based on the cosocket API 5 | 6 | Related project: 7 | [onion2web](https://github.com/starius/onion2web). 8 | 9 | [Paper](http://habrahabr.ru/post/243055/) (in Russian). 10 | 11 | Installation 12 | ------------ 13 | 14 | ```bash 15 | $ sudo luarocks install socks5 16 | ``` 17 | 18 | Reference 19 | --------- 20 | 21 | This module contains the following functions: 22 | 23 | * `socks5.auth(cosocket)` - authenticate to SOCKS5 24 | server (method "no authentication" is used). 25 | Cosocket must be connected to SOCKS5 server 26 | * `socks5.connect(cosocket, host, port)` - tell 27 | SOCKS5 server to connect to target host:port. 28 | Host must be domain name 29 | * `socks5.handle_request(socks5host, socks5port, 30 | request_changer?, response_changer?, change_only_html?)` - 31 | creates cosocket, authenticates to SOCKS5 server 32 | (defined by socks5host, socks5port), 33 | connects to target host:port (defined in ngx.req), 34 | receive request headers and body, send them 35 | through SOCKS5 server to target, 36 | then receive response headers and body, 37 | send them to client. 38 | 39 | Optional function `request_changer` is applied to 40 | request before sending it to target. 41 | Optional function `response_changer` is applied to 42 | response before sending it to client. 43 | 44 | The proxy can operate in two modes: 45 | 46 | * whole-page: read whole HTTP response and then 47 | send it to the client; 48 | * streaming: read response in small chunks. 49 | 50 | If `response_changer` is not used, streaming mode 51 | is used. If `response_changer` is used and 52 | `change_only_html` is truthy, then whole-page 53 | is used for HTML pages and streaming is used 54 | otherwise. 55 | 56 | How to use this module to proxy all requests through Tor: 57 | 58 | ```nginx 59 | server { 60 | listen 80; 61 | server_name ip4.me; # must be in request header 62 | location / { 63 | default_type text/html; 64 | content_by_lua ' 65 | require("socks5").handle_request("127.0.0.1", 9050) 66 | '; 67 | } 68 | } 69 | ``` 70 | -------------------------------------------------------------------------------- /socks5-dev-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "socks5" 2 | version = "dev-1" 3 | source = { 4 | url = 'git://github.com/starius/lua-resty-socks5', 5 | tag = 'master', 6 | } 7 | description = { 8 | summary = 9 | "Lua SOCKS5 client for the ngx_lua based " .. 10 | "on the cosocket API", 11 | homepage = "https://github.com/starius/lua-resty-socks5", 12 | maintainer = "Boris Nagaev ", 13 | license = "MIT" 14 | } 15 | dependencies = { 16 | "lua ~> 5.1" 17 | } 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | socks5 = "socks5.lua", 22 | }, 23 | } 24 | 25 | -------------------------------------------------------------------------------- /socks5.lua: -------------------------------------------------------------------------------- 1 | local ngx = require('ngx') 2 | 3 | local socks5 = {} 4 | 5 | local char = string.char 6 | 7 | -- magic numbers 8 | local SOCKS5 = 0x05 9 | local NUMBER_OF_AUTH_METHODS = 0x01 10 | local NO_AUTHENTICATION = 0x00 11 | local TCP_CONNECTION = 0x01 12 | local RESERVED = 0x00 13 | local IPv4 = 0x01 14 | local DOMAIN_NAME = 0x03 15 | local IPv6 = 0x04 16 | 17 | local REQUEST_GRANTED = 0x00 18 | local CONN_ERRORS = { 19 | [0x01] = 'general failure', 20 | [0x02] = 'connection not allowed by ruleset', 21 | [0x03] = 'network unreachable', 22 | [0x04] = 'host unreachable', 23 | [0x05] = 'connection refused by destination host', 24 | [0x06] = 'TTL expired', 25 | [0x07] = 'command not supported / protocol error', 26 | [0x08] = 'address type not supported', 27 | } 28 | 29 | local CHUNK_SIZE = 1024 30 | 31 | -- authentication to socks5 server 32 | socks5.auth = function(cosocket) 33 | cosocket:send(char(SOCKS5, NUMBER_OF_AUTH_METHODS, 34 | NO_AUTHENTICATION)) 35 | local auth_response = cosocket:receive(2) 36 | if auth_response ~= char(SOCKS5, NO_AUTHENTICATION) then 37 | return nil, "Socks5 authentification failed" 38 | end 39 | return true 40 | end 41 | 42 | -- connection request 43 | socks5.connect = function(cosocket, host, port) 44 | local host_length = #host 45 | local port_big_endian = char( 46 | math.floor(port / 256), port % 256) 47 | cosocket:send(char(SOCKS5, TCP_CONNECTION, RESERVED, 48 | DOMAIN_NAME, host_length) .. host .. port_big_endian) 49 | local conn_response = cosocket:receive(3) 50 | if conn_response ~= 51 | char(SOCKS5, REQUEST_GRANTED, RESERVED) then 52 | local status = conn_response:byte(2) 53 | local message = CONN_ERRORS[status] or 'Unknown error' 54 | return nil, message 55 | end 56 | -- pop address 57 | local addr_type = cosocket:receive(1) 58 | if addr_type == char(DOMAIN_NAME) then 59 | local addr_length = addr_type:byte(1) 60 | cosocket:receive(addr_length) 61 | elseif addr_type == char(IPv4) then 62 | cosocket:receive(4) 63 | elseif addr_type == char(IPv6) then 64 | cosocket:receive(16) 65 | else 66 | return nil, 'Bad address type: ' .. string.byte(addr_type) 67 | end 68 | -- pop port 69 | cosocket:receive(2) 70 | return true 71 | end 72 | 73 | socks5.handle_request = function(socks5host, socks5port, 74 | request_changer, response_changer, change_only_html) 75 | local sosocket = ngx.socket.connect(socks5host, socks5port) 76 | do 77 | local status, message = socks5.auth(sosocket) 78 | if not status then 79 | ngx.say('Error: ' .. message) 80 | return 81 | end 82 | end 83 | local target_host = ngx.req.get_headers()['Host'] 84 | if request_changer then 85 | target_host = request_changer(target_host) 86 | end 87 | local target_port = 80 88 | do 89 | local status, message = socks5.connect(sosocket, 90 | target_host, target_port) 91 | if not status then 92 | ngx.say('Error: ' .. message) 93 | return 94 | end 95 | end 96 | -- read request 97 | local clheader = ngx.req.raw_header() 98 | if request_changer then 99 | clheader = request_changer(clheader) 100 | end 101 | sosocket:send(clheader) 102 | ngx.req.read_body() 103 | local clbody = ngx.req.get_body_data() 104 | if clbody then 105 | if request_changer then 106 | clbody = request_changer(clbody) 107 | end 108 | sosocket:send(clbody) 109 | end 110 | -- read response 111 | local soheader, message = 112 | sosocket:receiveuntil('\r\n\r\n')() 113 | if not soheader then 114 | ngx.say('No headers received from target: ' .. message) 115 | return 116 | end 117 | local sobody_length = soheader:match( 118 | 'Content%-Length%: (%d+)') 119 | local is_html = soheader:match('Content%-Type: text/html') 120 | local change = is_html or not change_only_html 121 | local clsocket = ngx.req.socket(true) 122 | if response_changer and change then 123 | -- read whole body 124 | local sobody = sosocket:receive(sobody_length or '*a') or '' 125 | sobody = response_changer(sobody) 126 | soheader = response_changer(soheader) 127 | if soheader:find('Content%-Length%:') then 128 | soheader = soheader:gsub('Content%-Length%: %d+', 129 | 'Content-Length: ' .. #sobody) 130 | else 131 | soheader = soheader .. 132 | '\r\nContent-Length: ' .. #sobody 133 | end 134 | clsocket:send(soheader .. '\r\n\r\n' .. sobody) 135 | else 136 | -- stream 137 | clsocket:send(soheader .. '\r\n\r\n') 138 | while true do 139 | local sobody, _, partial = sosocket:receive(CHUNK_SIZE) 140 | if not sobody then 141 | clsocket:send(partial) 142 | break 143 | end 144 | local bytes = clsocket:send(sobody) 145 | if not bytes then 146 | break 147 | end 148 | end 149 | end 150 | -- close 151 | sosocket:close() 152 | end 153 | 154 | return socks5 155 | 156 | --------------------------------------------------------------------------------