Permalink
Browse files

Supports SOCKS5 proxy

  • Loading branch information...
kkazuo committed Oct 6, 2018
1 parent 91eeaa9 commit 00fdd7f9b55f7126d94cb519304d5b9492e83c5b
Showing with 172 additions and 5 deletions.
  1. +6 −0 README.markdown
  2. +90 −3 src/backend/usocket.lisp
  3. +12 −1 src/error.lisp
  4. +64 −1 t/dexador.lisp
View
@@ -198,6 +198,12 @@ You can connect via proxy.
(dex:get "http://lisp.org/" :proxy "http://proxy.yourcompany.com:8080/")
```
You can connect via SOCKS5 proxy.
```common-lisp
(dex:get "https://www.facebookcorewwwi.onion/" :proxy "socks5://127.0.0.1:9150")
```
## Functions
All functions take similar arguments.
View
@@ -13,7 +13,8 @@
:make-keep-alive-stream)
(:import-from :dexador.error
:http-request-failed
:http-request-not-found)
:http-request-not-found
:socks5-proxy-request-failed)
(:import-from :usocket
:socket-connect
:socket-stream)
@@ -374,6 +375,76 @@
(format nil "Basic ~A"
(string-to-base64-string proxy-auth)))))
(defconstant +socks5-version+ 5)
(defconstant +socks5-reserved+ 0)
(defconstant +socks5-no-auth+ 0)
(defconstant +socks5-connect+ 1)
(defconstant +socks5-domainname+ 3)
(defconstant +socks5-succeeded+ 0)
(defconstant +socks5-ipv4+ 1)
(defconstant +socks5-ipv6+ 4)
(defun ensure-socks5-connected (input output uri http-method)
(labels ((fail (condition &key reason)
(error (make-condition condition
:body nil :status nil :headers nil
:uri uri
:method http-method
:reason reason)))
(exact (n reason)
(unless (eql n (read-byte input nil 'eof))
(fail 'dexador.error:socks5-proxy-request-failed :reason reason)))
(drop (n reason)
(dotimes (i n)
(when (eq (read-byte input nil 'eof) 'eof)
(fail 'dexador.error:socks5-proxy-request-failed :reason reason)))))
;; Send Version + Auth Method
;; Currently, only supports no-auth method.
(write-byte +socks5-version+ output)
(write-byte 1 output)
(write-byte +socks5-no-auth+ output)
(finish-output output)
;; Receive Auth Method
(exact +socks5-version+ "Unexpected version")
(exact +socks5-no-auth+ "Unsupported auth method")
;; Send domainname Request
(let* ((host (babel:string-to-octets (uri-host uri)))
(hostlen (length host))
(port (uri-port uri)))
(unless (<= 1 hostlen 255)
(fail 'dexador.error:socks5-proxy-request-failed :reason "domainname too long"))
(unless (<= 1 port 65535)
(fail 'dexador.error:socks5-proxy-request-failed :reason "Invalid port"))
(write-byte +socks5-version+ output)
(write-byte +socks5-connect+ output)
(write-byte +socks5-reserved+ output)
(write-byte +socks5-domainname+ output)
(write-byte hostlen output)
(write-sequence host output)
(write-byte (ldb (byte 8 8) port) output)
(write-byte (ldb (byte 8 0) port) output)
(finish-output output)
;; Receive reply
(exact +socks5-version+ "Unexpected version")
(exact +socks5-succeeded+ "Unexpected result code")
(drop 1 "Should be reserved byte")
(let ((atyp (read-byte input nil 'eof)))
(cond
((eql atyp +socks5-ipv4+)
(drop 6 "Should be IPv4 address and port"))
((eql atyp +socks5-ipv6+)
(drop 18 "Should be IPv6 address and port"))
((eql atyp +socks5-domainname+)
(let ((n (read-byte input nil 'eof)))
(when (eq n 'eof)
(fail 'dexador.error:socks5-proxy-request-failed :reason "Invalid domainname length"))
(drop n "Should be domainname and port")))
(t
(fail 'dexador.error:socks5-proxy-request-failed :reason "Unknown address")))))))
(defun-careful request (uri &rest args
&key (method :get) (version 1.1)
content headers
@@ -387,7 +458,9 @@
want-stream
proxy
(insecure *not-verify-ssl*)
ca-path)
ca-path
&aux
(proxy-uri (and proxy (quri:uri proxy))))
(declare (ignorable ssl-key-file ssl-cert-file ssl-key-password
timeout ca-path)
(type single-float version)
@@ -403,6 +476,8 @@
:element-type '(unsigned-byte 8))))
(scheme (uri-scheme uri)))
(declare (type string scheme))
(when (socks5-proxy-p proxy-uri)
(ensure-socks5-connected stream stream uri method))
(if (string= scheme "https")
#+dexador-no-ssl
(error "SSL not supported. Remove :dexador-no-ssl from *features* to enable SSL.")
@@ -420,7 +495,7 @@
;; In executable environment, perhaps *ca-bundle* doesn't exist.
(t :default)))))
(cl+ssl:with-global-context (ctx :auto-free-p t)
(cl+ssl:make-ssl-client-stream (if proxy
(cl+ssl:make-ssl-client-stream (if (http-proxy-p proxy-uri)
(make-connect-stream uri version stream (make-proxy-authorization con-uri))
stream)
:hostname (uri-host uri)
@@ -433,6 +508,17 @@
:report "Retry the same request."
(return-from request
(apply #'request uri :use-connection-pool nil args)))))
(http-proxy-p (uri)
(and uri
(let ((scheme (uri-scheme uri)))
(and (stringp scheme)
(or (string= scheme "http")
(string= scheme "https"))))))
(socks5-proxy-p (uri)
(and uri
(let ((scheme (uri-scheme uri)))
(and (stringp scheme)
(string= scheme "socks5")))))
(connection-keep-alive-p (connection-header)
(and keep-alive
(or (and (= (the single-float version) 1.0)
@@ -446,6 +532,7 @@
(uri-authority uri)) stream)
(ignore-errors (close stream)))))
(let* ((uri (quri:uri uri))
(proxy (when (http-proxy-p proxy-uri) proxy))
(content-type
(find :content-type headers :key #'car :test #'eq))
(multipart-p (and (not content-type)
View
@@ -40,7 +40,10 @@
:response-status
:response-headers
:request-uri
:request-method))
:request-method
;; Proxy errors
:socks5-proxy-request-failed))
(in-package :dexador.error)
(define-condition http-request-failed (error)
@@ -115,3 +118,11 @@
:headers headers
:uri uri
:method method))
(define-condition socks5-proxy-request-failed (http-request-failed)
((reason :initarg :reason))
(:report (lambda (condition stream)
(with-slots (uri reason) condition
(format stream "An HTTP request to ~S via SOCKS5 has failed (reason=~S)."
(quri:render-uri uri)
reason)))))
View
@@ -8,7 +8,7 @@
:localhost))
(in-package :dexador-test)
(plan 17)
(plan 26)
(defun random-port ()
"Return a port number not in use from 50000 to 60000."
@@ -79,6 +79,69 @@
(is code 200)
(is body (localhost "/foo")))))
(subtest-app "proxy (socks5) case"
(flet ((check (uri in out)
(flexi-streams:with-input-from-sequence (in in)
(equalp
(flexi-streams:with-output-to-sequence (out :element-type '(unsigned-byte 8))
(dexador.backend.usocket::ensure-socks5-connected in out (quri:uri uri) :get))
out))))
(ok (check "http://example.com/"
#(5 0
5 0 0 1 0 0 0 0 0 0)
#(5 1 0
5 1 0 3 11 101 120 97 109 112 108 101 46 99 111 109 0 80)))
(ok (check "https://example.com/"
#(5 0
5 0 0 1 0 0 0 0 0 0)
#(5 1 0
5 1 0 3 11 101 120 97 109 112 108 101 46 99 111 109 1 187)))
(ok (check "http://example.com:8080/"
#(5 0
5 0 0 1 0 0 0 0 0 0)
#(5 1 0
5 1 0 3 11 101 120 97 109 112 108 101 46 99 111 109 31 144)))
(ok (check "https://example.com:8080/"
#(5 0
5 0 0 1 0 0 0 0 0 0)
#(5 1 0
5 1 0 3 11 101 120 97 109 112 108 101 46 99 111 109 31 144)))
(ok (check "http://example.com/"
#(5 0
5 0 0 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
#(5 1 0
5 1 0 3 11 101 120 97 109 112 108 101 46 99 111 109 0 80)))
(ok (check "http://example.com/"
#(5 0
5 0 0 3 1 0 0 0)
#(5 1 0
5 1 0 3 11 101 120 97 109 112 108 101 46 99 111 109 0 80)))
(handler-case
(check "http://example.com/"
#(4)
#())
(dex:socks5-proxy-request-failed ()
(ok t)))
(handler-case
(check "http://example.com/"
#(5 255)
#())
(dex:socks5-proxy-request-failed ()
(ok t))))
#+needs-Tor-running-on-localhost
(let ((proxy "socks5://127.0.0.1:9150"))
(subtest "SOCKS5 GET"
(multiple-value-bind (body code)
(dex:get "http://duskgytldkxiuqc6.onion/" :proxy proxy)
(declare (ignore body))
(is code 200)))
(subtest "SOCKS5 GET with SSL"
(multiple-value-bind (body code)
(dex:get "https://www.facebookcorewwwi.onion/" :proxy proxy)
(declare (ignore body))
(is code 200)))))
(subtest-app "redirection"
(lambda (env)
(let ((id (parse-integer (subseq (getf env :path-info) 1))))

0 comments on commit 00fdd7f

Please sign in to comment.