Skip to content
This repository was archived by the owner on Jan 14, 2022. It is now read-only.

Commit b0a2887

Browse files
authored
Merge pull request #21 from tootsuite/improve-security
Improve security to match Node.js version
2 parents 839f04a + 1765dc3 commit b0a2887

4 files changed

Lines changed: 109 additions & 32 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main.rs

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ use receiver::Receiver;
4040
use std::env;
4141
use std::net::SocketAddr;
4242
use stream::StreamManager;
43-
use user::{Scope, User};
43+
use user::{OauthScope::*, Scope, User};
4444
use warp::path;
4545
use warp::Filter as WarpFilter;
4646

@@ -110,37 +110,62 @@ fn main() {
110110
h: query::Hashtag,
111111
l: query::List,
112112
ws: warp::ws::Ws2| {
113-
let unauthorized = Err(warp::reject::custom("Error: Invalid Access Token"));
113+
let scopes = user.scopes.clone();
114114
let timeline = match q.stream.as_ref() {
115115
// Public endpoints:
116116
tl @ "public" | tl @ "public:local" if m.is_truthy() => format!("{}:media", tl),
117117
tl @ "public:media" | tl @ "public:local:media" => tl.to_string(),
118118
tl @ "public" | tl @ "public:local" => tl.to_string(),
119-
// User
120-
"user" if user.id == -1 => return unauthorized,
121-
"user" => format!("{}", user.id),
122-
"user:notification" => {
123-
user = user.with_notification_filter();
124-
format!("{}", user.id)
125-
}
126119
// Hashtag endpoints:
127120
// TODO: handle missing query
128121
tl @ "hashtag" | tl @ "hashtag:local" => format!("{}:{}", tl, h.tag),
122+
// Private endpoints: User
123+
"user"
124+
if user.id > 0
125+
&& (scopes.contains(&Read) || scopes.contains(&ReadStatuses)) =>
126+
{
127+
format!("{}", user.id)
128+
}
129+
"user:notification"
130+
if user.id > 0
131+
&& (scopes.contains(&Read) || scopes.contains(&ReadNotifications)) =>
132+
{
133+
user = user.with_notification_filter();
134+
format!("{}", user.id)
135+
}
129136
// List endpoint:
130137
// TODO: handle missing query
131-
"list" if user.authorized_for_list(l.list).is_err() => return unauthorized,
132-
"list" => format!("list:{}", l.list),
138+
"list"
139+
if user.authorized_for_list(l.list).is_ok()
140+
&& (scopes.contains(&Read) || scopes.contains(&ReadList)) =>
141+
{
142+
format!("list:{}", l.list)
143+
}
144+
133145
// Direct endpoint:
134-
"direct" if user.id == -1 => return unauthorized,
135-
"direct" => "direct".to_string(),
146+
"direct"
147+
if user.id > 0
148+
&& (scopes.contains(&Read) || scopes.contains(&ReadStatuses)) =>
149+
{
150+
"direct".to_string()
151+
}
152+
// Reject unathorized access attempts for private endpoints
153+
"user" | "user:notification" | "direct" | "list" => {
154+
return Err(warp::reject::custom("Error: Invalid Access Token"))
155+
}
136156
// Other endpoints don't exist:
137157
_ => return Err(warp::reject::custom("Error: Nonexistent WebSocket query")),
138158
};
159+
let token = user.access_token.clone();
139160
let stream = redis_updates_ws.configure_copy(&timeline, user);
140161

141-
Ok(ws.on_upgrade(move |socket| ws::send_replies(socket, stream)))
162+
Ok((
163+
ws.on_upgrade(move |socket| ws::send_replies(socket, stream)),
164+
token,
165+
))
142166
},
143-
);
167+
)
168+
.map(|(reply, token)| warp::reply::with_header(reply, "sec-websocket-protocol", token));
144169

145170
let address: SocketAddr = env::var("SERVER_ADDR")
146171
.unwrap_or("127.0.0.1:4000".to_owned())

src/stream.rs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ impl Stream for StreamManager {
5151
type Error = Error;
5252

5353
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
54-
let mut receiver = self.receiver.lock().expect("No other thread panic");
54+
let mut receiver = self
55+
.receiver
56+
.lock()
57+
.expect("StreamManager: No other thread panic");
5558
receiver.update(self.id, &self.target_timeline.clone());
5659
match receiver.poll() {
5760
Ok(Async::Ready(Some(value))) => {
@@ -61,19 +64,19 @@ impl Stream for StreamManager {
6164
.expect("Previously set current user");
6265

6366
let user_langs = user.langs.clone();
64-
let copy = value.clone();
65-
let event = copy["event"].as_str().expect("Redis string");
66-
let copy = value.clone();
67-
let payload = copy["payload"].to_string();
68-
let copy = value.clone();
69-
let toot_lang = copy["payload"]["language"]
70-
.as_str()
71-
.expect("redis str")
72-
.to_string();
67+
let event = value["event"].as_str().expect("Redis string");
68+
let payload = value["payload"].to_string();
7369

7470
match (&user.filter, user_langs) {
7571
(Filter::Notification, _) if event != "notification" => Ok(Async::NotReady),
76-
(Filter::Language, Some(ref langs)) if !langs.contains(&toot_lang) => {
72+
(Filter::Language, Some(ref user_langs))
73+
if !user_langs.contains(
74+
&value["payload"]["language"]
75+
.as_str()
76+
.expect("Redis str")
77+
.to_string(),
78+
) =>
79+
{
7780
Ok(Async::NotReady)
7881
}
7982
_ => Ok(Async::Ready(Some(json!(

src/user.rs

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,35 +27,69 @@ pub enum Filter {
2727
#[derive(Clone, Debug, PartialEq)]
2828
pub struct User {
2929
pub id: i64,
30+
pub access_token: String,
31+
pub scopes: Vec<OauthScope>,
3032
pub langs: Option<Vec<String>>,
3133
pub logged_in: bool,
3234
pub filter: Filter,
3335
}
36+
#[derive(Clone, Debug, PartialEq)]
37+
pub enum OauthScope {
38+
Read,
39+
ReadStatuses,
40+
ReadNotifications,
41+
ReadList,
42+
Other,
43+
}
44+
impl From<&str> for OauthScope {
45+
fn from(scope: &str) -> Self {
46+
use OauthScope::*;
47+
match scope {
48+
"read" => Read,
49+
"read:statuses" => ReadStatuses,
50+
"read:notifications" => ReadNotifications,
51+
"read:lists" => ReadList,
52+
_ => Other,
53+
}
54+
}
55+
}
3456
impl User {
3557
/// Create a user from the access token supplied in the header or query paramaters
36-
pub fn from_access_token(token: String, scope: Scope) -> Result<Self, warp::reject::Rejection> {
58+
pub fn from_access_token(
59+
access_token: String,
60+
scope: Scope,
61+
) -> Result<Self, warp::reject::Rejection> {
3762
let conn = connect_to_postgres();
3863
let result = &conn
3964
.query(
4065
"
41-
SELECT oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages
66+
SELECT oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages, oauth_access_tokens.scopes
4267
FROM
4368
oauth_access_tokens
4469
INNER JOIN users ON
4570
oauth_access_tokens.resource_owner_id = users.id
4671
WHERE oauth_access_tokens.token = $1
4772
AND oauth_access_tokens.revoked_at IS NULL
4873
LIMIT 1",
49-
&[&token],
74+
&[&access_token],
5075
)
5176
.expect("Hard-coded query will return Some([0 or more rows])");
5277
if !result.is_empty() {
5378
let only_row = result.get(0);
5479
let id: i64 = only_row.get(1);
80+
let scopes = only_row
81+
.get::<_, String>(3)
82+
.split(' ')
83+
.map(|scope: &str| scope.into())
84+
.filter(|scope| scope != &OauthScope::Other)
85+
.collect();
86+
dbg!(&scopes);
5587
let langs: Option<Vec<String>> = only_row.get(2);
5688
info!("Granting logged-in access");
5789
Ok(User {
5890
id,
91+
access_token,
92+
scopes,
5993
langs,
6094
logged_in: true,
6195
filter: Filter::None,
@@ -64,6 +98,8 @@ LIMIT 1",
6498
info!("Granting public access to non-authenticated client");
6599
Ok(User {
66100
id: -1,
101+
access_token,
102+
scopes: Vec::new(),
67103
langs: None,
68104
logged_in: false,
69105
filter: Filter::None,
@@ -116,6 +152,8 @@ LIMIT 1",
116152
pub fn public() -> Self {
117153
User {
118154
id: -1,
155+
access_token: String::new(),
156+
scopes: Vec::new(),
119157
langs: None,
120158
logged_in: false,
121159
filter: Filter::None,
@@ -130,16 +168,25 @@ pub enum Scope {
130168
}
131169
impl Scope {
132170
pub fn get_access_token(self) -> warp::filters::BoxedFilter<(String,)> {
133-
let token_from_header = warp::header::header::<String>("authorization")
171+
let token_from_header_http_push = warp::header::header::<String>("authorization")
134172
.map(|auth: String| auth.split(' ').nth(1).unwrap_or("invalid").to_string());
173+
let token_from_header_ws =
174+
warp::header::header::<String>("Sec-WebSocket-Protocol").map(|auth: String| auth);
135175
let token_from_query = warp::query().map(|q: query::Auth| q.access_token);
176+
177+
let private_scopes = any_of!(
178+
token_from_header_http_push,
179+
token_from_header_ws,
180+
token_from_query
181+
);
182+
136183
let public = warp::any().map(|| "no access token".to_string());
137184

138185
match self {
139186
// if they're trying to access a private scope without an access token, reject the request
140-
Scope::Private => any_of!(token_from_query, token_from_header).boxed(),
187+
Scope::Private => private_scopes.boxed(),
141188
// if they're trying to access a public scope without an access token, proceed
142-
Scope::Public => any_of!(token_from_query, token_from_header, public).boxed(),
189+
Scope::Public => any_of!(private_scopes, public).boxed(),
143190
}
144191
}
145192
}

0 commit comments

Comments
 (0)