Skip to content

Commit

Permalink
SOCKS5 proxy for HTTP-REQUEST
Browse files Browse the repository at this point in the history
  • Loading branch information
gonzojive committed Aug 6, 2010
1 parent 86785ae commit 8690074
Show file tree
Hide file tree
Showing 2 changed files with 250 additions and 22 deletions.
186 changes: 186 additions & 0 deletions cl-socks.lisp
@@ -0,0 +1,186 @@
(defpackage :cl-socks
(:nicknames :socks)
(:use :cl)
(:export #:socks-connect))

(in-package :cl-socks)


(defconstant +socks5-no-authorization+ 0
"Authentication method that requires no credentials.")

(defconstant +socks5-version-byte+ 5
"Authentication method that requires no credentials.")

(defconstant +socks5-address-type-ipv4+ 1
"IPV4")

(defconstant +socks5-address-type-domainname+ 3
"Domain name address type byte")

(defconstant +socks5-address-type-ipv6+ 4
"IPV6")


(defconstant +socks5-version-byte+ 5
"Authentication method that requires no credentials.")

(defconstant +socks5-version-byte+ 5
"Authentication method that requires no credentials.")

(defclass socks-client ()
((underlying-stream :initarg :underlying-stream :reader client-underlying-stream
:documentation "The STREAM object this object wraps")
))

(defun wrap-stream (wrapper-class underlying-stream &rest initargs)
(apply 'make-instance wrapper-class :underlying-stream underlying-stream initargs))

(defun socks-connect (stream destination-address destination-port)
"Establishes a SOCKS5 connection to DESTINATION-ADDRESS on
DESTINATION-PORT through (socket) binary stream STREAM. After
this (blocking) function completes, the stream should behave mostly
as if a direct connection had been made to the desination.
DESTINATION-ADDRESS is either a Fully Qualified Domain Name (including
the dot at the end and less than 255 characters) string, a vector of 4
bytes (ipv4 address), or a vector of 6 bytes (ipv6 address).
Returns (BOUND-ADDRESS-TYPE BOUND-ADDRESS BOUND-PORT) of the
connection on the proxy server.
See http://www.faqs.org/rfcs/rfc1928.html"
(socks-client-connect (wrap-stream 'socks-client stream)
destination-address destination-port))

(defun destructure-user-address (address)
"Takes as input a string or byte array and determines the type of
desination address specified. Returns 2 values: its type (one
of :ipv4 :ipv6 or :domainname) and the processed address that is valid
to pass into SOCKS-REQUEST."
(typecase address
(string (values :domainname address))
(sequence (case (length address)
(4 (values :ipv4 address))
(6 (values :ipv6 address))
(t (error "Invalid Address ~A" address))))
(t (error "Invalid Address ~A" address))))

(defun socks-client-connect (client destination-address destination-port)
(socks-authenticate client)
(multiple-value-bind (bound-address-type bound-address port)
(socks-request client :connect :domainname destination-address destination-port)
#+nil
(format t "Successfully connected to socks server bound to ~A ~A ~A!~%"
bound-address-type bound-address port)
(values bound-address-type bound-address port)))


(defmacro with-syntax-sugar ((socks-stream-var) &body body)
`(labels ((expect-byte (expected &optional (error-format "Read unexpected SOCKS stream byte value ~A"))
(let ((value (read-byte ,socks-stream-var)))
(unless (eql expected value)
(error error-format value))
expected))

(read-n-bytes (n)
(let ((array (make-array n :element-type '(unsigned-byte 8))))
(unless (eql n (read-sequence array ,socks-stream-var))
(error "Failed to read ~A bytes from SOCKS stream" n))
array))

(expect-version-byte ()
(expect-byte +socks5-version-byte+ "Invalid SOCKS connection/version ~A")))
,@body))

(defun socks-authenticate (client)
"Performs handshake using the underlying stream."
(let ((socks-stream (client-underlying-stream client)))
(with-syntax-sugar (socks-stream)
;; http://www.faqs.org/rfcs/rfc1928.html

;; Send acceptable authentication methods. only support no
;; authorization for now
(write-sequence (vector +socks5-version-byte+ 1 +socks5-no-authorization+) socks-stream)
(finish-output socks-stream)

(expect-version-byte)
(expect-byte +socks5-no-authorization+)

client)))

(defun socks-request (client command address-type address port)
(declare (type (member :connect) command)
(type (member :ipv4 :ipv6 :domainname) address-type)
(type integer port)
(type socks-client client))
(let ((socks-stream (client-underlying-stream client)))
(with-syntax-sugar (socks-stream)
;; write version, cmd, rsv
(write-sequence (vector +socks5-version-byte+
(case command
(:connect 1)
(:bind 2)
(:udp-associate 3))
0)
socks-stream)

;; write the address type
(case address-type
(:ipv4 (write-sequence (concatenate 'vector
(list +socks5-address-type-ipv4+)
address)
socks-stream))
(:ipv6 (write-sequence (concatenate 'vector
(list +socks5-address-type-ipv6+)
address)
socks-stream))
(:domainname (let ((octets (flexi-streams:string-to-octets address)))
(unless (<= (length octets) 255)
(error "Fully qualified domain name too long"))
(when (not (eql #\. (elt address (- (length octets) 1))))
(error "Fully qualified domain name must end in '.'"))
(write-sequence (concatenate 'vector
(list +socks5-address-type-domainname+
(length octets))
octets)
socks-stream))))

;; write the port number
(unless (> (expt 2 16) port -1)
(error "Invalid port number ~A" port))

(write-sequence (vector (ldb (byte 8 8) port)
(ldb (byte 8 0) port))
socks-stream)

(finish-output socks-stream)

;;; Receive the response
(expect-version-byte)
(expect-byte 0 "SOCKS5 error during connect. Code: ~A")
(read-byte socks-stream)

(multiple-value-bind (server-bound-address-type server-bound-address)
(let ((type-byte (read-byte socks-stream)))
(cond
((eql type-byte +socks5-address-type-ipv4+)
(values :ipv4
(read-n-bytes 4)))
((eql type-byte +socks5-address-type-ipv6+)
(values :ipv6
(read-n-bytes 6)))

((eql type-byte +socks5-address-type-domainname+)
(values :domainname
(let ((addr-len (read-byte socks-stream)))
(flexi-streams:octets-to-string (read-n-bytes addr-len)))))

(t (error "Invalid address type byte received ~A" type-byte))))

(let* ((port-bytes (read-n-bytes 2))
(port (+ (ash (elt port-bytes 0) 8)
(elt port-bytes 1))))
#+nil
(format t "Connected ~A ~A ~A~%" server-bound-address-type server-bound-address port)
(values server-bound-address-type server-bound-address port))))))
86 changes: 64 additions & 22 deletions request.lisp
Expand Up @@ -188,6 +188,40 @@ second value."
result)))
(chunked-input-stream-trailers (flexi-stream-stream stream)))))

(defun drakma-open-tcp-stream (host port &key socks-host socks-port
#+:lispworks connection-timeout
#+:lispworks read-timeout
#+(and :lispworks (not :lw-does-not-have-write-timeout)) write-timeout)
"Creates a TCP stream and returns it."
(declare (optimize (debug 3)))
(flet ((create-stream (host port)
"This is a raw socket stream creation function that we wrap
with various other functionality like SOCKS negotiation."
#+:lispworks
(comm:open-tcp-stream host port
:element-type 'octet
:timeout connection-timeout
:read-timeout read-timeout
#-:lw-does-not-have-write-timeout
:write-timeout
#-:lw-does-not-have-write-timeout
write-timeout
:errorp t)
#-:lispworks
(usocket:socket-stream
(usocket:socket-connect host port
:element-type 'octet
#+:openmcl :deadline
#+:openmcl deadline
:nodelay t))))
(if socks-host
(let* ((socks-stream (create-stream socks-host socks-port)))
(socks:socks-connect socks-stream
(concatenate 'string host ".")
port)
socks-stream)
(create-stream host port))))

(defun http-request (uri &rest args
&key (protocol :http/1.1)
(method :get)
Expand All @@ -205,6 +239,7 @@ second value."
(decompress t)
proxy
proxy-basic-authorization
socks-proxy
additional-headers
(redirect 5)
(redirect-methods '(:get :head))
Expand Down Expand Up @@ -338,13 +373,19 @@ decompressed body sequence or string; if WANT-STREAM is T, then
BODY-OR-STREAM is a flexi-stream backed by a gzip-input-stream, which
in turn wraps the socket stream.
If PROXY is not NIL, it should be a string denoting a proxy
server through which the request should be sent. Or it can be a
list of two values - a string denoting the proxy server and an
integer denoting the port to use \(which will default to 80
otherwise). PROXY-BASIC-AUTHORIZATION is used like
BASIC-AUTHORIZATION, but for the proxy, and only if PROXY is
true.
If PROXY is not NIL, it should be a string denoting an HTTP proxy
server through which the request should be sent. Or it can be a list
of two values -- a string denoting the proxy server and an integer
denoting the port to use \(which will default to 80 otherwise).
PROXY-BASIC-AUTHORIZATION is used like BASIC-AUTHORIZATION, but for
the proxy, and only if PROXY is true.
If SOCKS-PROXY is not NIL, it should be a string denoting a SOCKSu5
proxy server through which the request should be sent. Or it can be a
list of two values -- a string denoting the proxy server and an
integer denoting the port to use \(which will default to 1080
otherwise). If both PROXY and SOCKS-PROXY are specified, then the
request goes through the SOCKS5 proxy and then through the HTTP proxy.
ADDITIONAL-HEADERS is a name/value alist of additional HTTP headers
which should be sent with the request. Unlike in PARAMETERS, the cdrs
Expand Down Expand Up @@ -430,6 +471,10 @@ only available on CCL 1.2 and later."
(when proxy
(when (atom proxy)
(setq proxy (list proxy 80))))
;; convert SOCKS-PROXY argument to canonical form
(when socks-proxy
(when (atom socks-proxy)
(setq socks-proxy (list socks-proxy 1080))))
;; make sure we don't get :CRLF on Windows
(let ((*default-eol-style* :lf)
(file-parameters-p (find-if-not #'stringp parameters :key #'cdr))
Expand Down Expand Up @@ -470,22 +515,19 @@ only available on CCL 1.2 and later."
(setq write-timeout nil))
(setq http-stream (or stream
#+:lispworks
(comm:open-tcp-stream host port
:element-type 'octet
:timeout connection-timeout
:read-timeout read-timeout
#-:lw-does-not-have-write-timeout
:write-timeout
#-:lw-does-not-have-write-timeout
write-timeout
:errorp t)
(drakma-open-tcp-stream host port
:socks-host (first socks-proxy)
:socks-port (second socks-proxy)
:timeout connection-timeout
:read-timeout read-timeout
#-:lw-does-not-have-write-timeout
:write-timeout
#-:lw-does-not-have-write-timeout
write-timeout)
#-:lispworks
(usocket:socket-stream
(usocket:socket-connect host port
:element-type 'octet
#+:openmcl :deadline
#+:openmcl deadline
:nodelay t))))
(drakma-open-tcp-stream host port
:socks-host (first socks-proxy)
:socks-port (second socks-proxy))))
#+:openmcl
(when deadline
;; it is correct to set the deadline here even though
Expand Down

0 comments on commit 8690074

Please sign in to comment.