4
4
5
5
from . import config
6
6
from .web import app
7
-
8
7
from .subrequest import make_subrequest
9
8
10
- import pyonionreq
11
-
12
- _junk_parser = pyonionreq .junk .Parser (
13
- privkey = config .PRIVKEYS ["onionreq" ].encode (), pubkey = config .PUBKEYS ["onionreq" ].encode ()
14
- )
9
+ from session_util .onionreq import OnionReqParser
15
10
11
+ onion_privkey_bytes = config .PRIVKEYS ["onionreq" ].encode ()
12
+ onion_pubkey_bytes = config .PUBKEYS ["onionreq" ].encode ()
16
13
17
14
def bencode_consume_string (body : memoryview ) -> Tuple [memoryview , memoryview ]:
18
15
"""
@@ -35,6 +32,64 @@ def bencode_consume_string(body: memoryview) -> Tuple[memoryview, memoryview]:
35
32
36
33
37
34
def handle_v4_onionreq_plaintext (body ):
35
+ try :
36
+ if not (body .startswith (b'l' ) and body .endswith (b'e' )):
37
+ raise RuntimeError ("Invalid onion request body: expected bencoded list" )
38
+
39
+ belems = memoryview (body )[1 :- 1 ]
40
+
41
+ # Metadata json; this element is always required:
42
+ meta , belems = bencode_consume_string (belems )
43
+
44
+ meta = json .loads (meta .tobytes ())
45
+
46
+ # Then we can have a second optional string containing the body:
47
+ if len (belems ) > 1 :
48
+ subreq_body , belems = bencode_consume_string (belems )
49
+ if len (belems ):
50
+ raise RuntimeError ("Invalid v4 onion request: found more than 2 parts" )
51
+ else :
52
+ subreq_body = b''
53
+
54
+ method , endpoint = meta ['method' ], meta ['endpoint' ]
55
+ if not endpoint .startswith ('/' ):
56
+ raise RuntimeError ("Invalid v4 onion request: endpoint must start with /" )
57
+
58
+ response , headers = make_subrequest (
59
+ method , endpoint , headers = meta .get ('headers' , {}), body = subreq_body
60
+ )
61
+
62
+ data = response .get_data ()
63
+ app .logger .debug (
64
+ f"Onion sub-request for { endpoint } returned { response .status_code } , { len (data )} bytes"
65
+ )
66
+
67
+ meta = {'code' : response .status_code , 'headers' : headers }
68
+
69
+ except Exception as e :
70
+ app .logger .warning ("Invalid v4 onion request: {}" .format (e ))
71
+ meta = {'code' : http .BAD_REQUEST , 'headers' : {'content-type' : 'text/plain; charset=utf-8' }}
72
+ data = b'Invalid v4 onion request'
73
+
74
+ meta = json .dumps (meta ).encode ()
75
+ return b'' .join (
76
+ (b'l' , str (len (meta )).encode (), b':' , meta , str (len (data )).encode (), b':' , data , b'e' )
77
+ )
78
+
79
+
80
+ def decrypt_onionreq ():
81
+ try :
82
+ return OnionReqParser (
83
+ onion_pubkey_bytes ,
84
+ onion_privkey_bytes ,
85
+ request .data )
86
+ except Exception as e :
87
+ app .logger .warning ("Failed to decrypt onion request: {}" .format (e ))
88
+ abort (http .BAD_REQUEST )
89
+
90
+
91
+ @app .post ("/oxen/v4/lsrpc" )
92
+ def handle_v4_onion_request ():
38
93
"""
39
94
Handles a decrypted v4 onion request; this injects a subrequest to process it then returns the
40
95
result of that subrequest. In contrast to v3, it is more efficient (particularly for binary
@@ -54,11 +109,6 @@ def handle_v4_onionreq_plaintext(body):
54
109
needs to be accessed through a v4 request for some reason then it can be accessed via the
55
110
"/legacy/whatever" endpoint).
56
111
57
- If an "endpoint" contains unicode characters then it is recommended to provide it as direct
58
- UTF-8 values (rather than URL-encoded UTF-8). Both approaches will work, but the X-SOGS-*
59
- authentication headers will always apply on the final, URL-decoded value and so avoiding
60
- URL-encoding in the first place will typically simplify client implementations.
61
-
62
112
The "headers" field typically carries X-SOGS-* authentication headers as well as fields like
63
113
Content-Type. Note that, unlike v3 requests, the Content-Type does *not* have any default and
64
114
should also be specified, often as `application/json`. Unlike HTTP requests, Content-Length is
@@ -128,61 +178,6 @@ def handle_v4_onionreq_plaintext(body):
128
178
bytes are returned directly to the client (i.e. no base64 encoding applied, unlike v3 requests).
129
179
""" # noqa: E501
130
180
131
- try :
132
- if not (body .startswith (b"l" ) and body .endswith (b"e" )):
133
- raise RuntimeError ("Invalid onion request body: expected bencoded list" )
134
-
135
- belems = memoryview (body )[1 :- 1 ]
136
-
137
- # Metadata json; this element is always required:
138
- meta , belems = bencode_consume_string (belems )
139
-
140
- meta = json .loads (meta .tobytes ())
141
-
142
- # Then we can have a second optional string containing the body:
143
- if len (belems ) > 1 :
144
- subreq_body , belems = bencode_consume_string (belems )
145
- if len (belems ):
146
- raise RuntimeError ("Invalid v4 onion request: found more than 2 parts" )
147
- else :
148
- subreq_body = b""
149
-
150
- method , endpoint = meta ["method" ], meta ["endpoint" ]
151
- if not endpoint .startswith ("/" ):
152
- raise RuntimeError ("Invalid v4 onion request: endpoint must start with /" )
153
-
154
- response , headers = make_subrequest (
155
- method ,
156
- endpoint ,
157
- headers = meta .get ("headers" , {}),
158
- body = subreq_body ,
159
- user_reauth = True , # Because onion requests have auth headers on the *inside*
160
- )
161
-
162
- data = response .get_data ()
163
- app .logger .debug (
164
- f"Onion sub-request for { endpoint } returned { response .status_code } , { len (data )} bytes"
165
- )
166
-
167
- meta = {"code" : response .status_code , "headers" : headers }
168
-
169
- except Exception as e :
170
- app .logger .warning ("Invalid v4 onion request: {}" .format (e ))
171
- meta = {"code" : 400 , "headers" : {"content-type" : "text/plain; charset=utf-8" }}
172
- data = b"Invalid v4 onion request"
173
-
174
- meta = json .dumps (meta ).encode ()
175
- return b"" .join (
176
- (b"l" , str (len (meta )).encode (), b":" , meta , str (len (data )).encode (), b":" , data , b"e" )
177
- )
178
-
179
-
180
- @app .post ("/oxen/v4/lsrpc" )
181
- def handle_v4_onion_request ():
182
- """
183
- Parse a v4 onion request. See handle_v4_onionreq_plaintext().
184
- """
185
-
186
181
# Some less-than-ideal decisions in the onion request protocol design means that we are stuck
187
182
# dealing with parsing the request body here in the internal format that is meant for storage
188
183
# server, but the *last* hop's decrypted, encoded data has to get shared by us (and is passed on
@@ -198,13 +193,13 @@ def handle_v4_onion_request():
198
193
# The parse_junk here takes care of decoding and decrypting this according to the fields *meant
199
194
# for us* in the json (which include things like the encryption type and ephemeral key):
200
195
try :
201
- junk = _junk_parser . parse_junk ( request . data )
196
+ parser = decrypt_onionreq ( )
202
197
except RuntimeError as e :
203
198
app .logger .warning ("Failed to decrypt onion request: {}" .format (e ))
204
199
abort (400 )
205
200
206
201
# On the way back out we re-encrypt via the junk parser (which uses the ephemeral key and
207
202
# enc_type that were specified in the outer request). We then return that encrypted binary
208
203
# payload as-is back to the client which bounces its way through the SN path back to the client.
209
- response = handle_v4_onionreq_plaintext (junk .payload )
210
- return junk . transformReply (response )
204
+ response = handle_v4_onionreq_plaintext (parser .payload )
205
+ return parser . encrypt_reply (response )
0 commit comments