Skip to content

Commit 4ae4169

Browse files
committed
Authenticate requests between components
Use a randomly generated token to authenticate requests to the server for all endpoints, including WebSockets. This can be circumvented by inspecting the command line of the process to discover the token; however, it is better than nothing.
1 parent a10db15 commit 4ae4169

17 files changed

+162
-27
lines changed

Uebersicht/UBAppDelegate.m

Lines changed: 69 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,13 @@ @implementation UBAppDelegate {
3232
UBWidgetsStore* widgetsStore;
3333
UBWidgetsController* widgetsController;
3434
BOOL needsRefresh;
35+
NSString *token;
3536
}
3637

3738
@synthesize statusBarMenu;
3839

40+
static const uint TOKEN_LENGTH_256_BITS = 32;
41+
3942
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
4043
{
4144
needsRefresh = YES;
@@ -104,24 +107,49 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
104107

105108
// start server and load webview
106109
portOffset = 0;
110+
token = generateToken(TOKEN_LENGTH_256_BITS);
107111
[self startUp];
108112

109113
[self listenToWallpaperChanges];
110114
}
111115

112-
- (NSDictionary*)fetchState
116+
- (void)fetchState:(void (^)(NSDictionary*))callback
113117
{
114-
[[UBWebSocket sharedSocket] open:[self serverUrl:@"ws"]];
118+
[[UBWebSocket sharedSocket] open:[self serverUrl:@"ws"]
119+
token:token];
120+
115121
NSURL *urlPath = [[self serverUrl:@"http"] URLByAppendingPathComponent: @"state/"];
116-
NSData *jsonData = [NSData dataWithContentsOfURL:urlPath];
117-
NSError *error = nil;
118-
NSDictionary *dataDictionary = [NSJSONSerialization
119-
JSONObjectWithData: jsonData
120-
options: NSJSONReadingMutableContainers
121-
error: &error
122-
];
123-
if (error) NSLog(@"%@", error);
124-
return dataDictionary;
122+
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:urlPath];
123+
[request setValue:@"Übersicht" forHTTPHeaderField:@"Origin"];
124+
[request setValue:[NSString stringWithFormat:@"token=%@", token] forHTTPHeaderField:@"Cookie"];
125+
NSURLSessionDataTask *t = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
126+
if (error) {
127+
NSLog(@"error loading state: %@", error);
128+
dispatch_async(dispatch_get_main_queue(), ^{
129+
callback(@{});
130+
});
131+
return;
132+
}
133+
134+
NSError *jsonError = nil;
135+
NSDictionary *dataDictionary = [NSJSONSerialization
136+
JSONObjectWithData: data
137+
options: NSJSONReadingMutableContainers
138+
error: &jsonError
139+
];
140+
if (jsonError) {
141+
NSLog(@"error parsing state: %@", error);
142+
dispatch_async(dispatch_get_main_queue(), ^{
143+
callback(@{});
144+
});
145+
return;
146+
}
147+
148+
dispatch_async(dispatch_get_main_queue(), ^{
149+
callback(dataDictionary);
150+
});
151+
}];
152+
[t resume];
125153
}
126154

127155
- (void)startUp
@@ -132,10 +160,11 @@ - (void)startUp
132160
void (^handleData)(NSString*) = ^(NSString* output) {
133161
// note that these might be called several times
134162
if ([output rangeOfString:@"server started"].location != NSNotFound) {
135-
[self->widgetsStore reset: [self fetchState]];
136-
// this will trigger a render
137-
[self->screensController syncScreens:self];
138-
163+
[self fetchState:^(NSDictionary* state) {
164+
[self->widgetsStore reset: state];
165+
// this will trigger a render
166+
[self->screensController syncScreens:self];
167+
}];
139168
} else if ([output rangeOfString:@"EADDRINUSE"].location != NSNotFound) {
140169
self->portOffset++;
141170
}
@@ -243,6 +272,7 @@ - (NSTask*)launchWidgetServer:(NSString*)widgetPath
243272
@"-d", widgetPath,
244273
@"-p", [NSString stringWithFormat:@"%d", PORT + portOffset],
245274
@"-s", [[self getPreferencesDir] path],
275+
@"-t", token,
246276
loginShell ? @"--login-shell" : @""
247277
]];
248278

@@ -285,6 +315,7 @@ - (void)screensChanged:(NSDictionary*)screens
285315
[windowsController
286316
updateWindows:screens
287317
baseUrl: [self serverUrl: @"http"]
318+
token:token
288319
interactionEnabled: preferences.enableInteraction
289320
forceRefresh: needsRefresh
290321
];
@@ -444,6 +475,29 @@ void wallpaperSettingsChanged(
444475
}
445476
}
446477

478+
/*!
479+
@function generateToken
480+
481+
@abstract
482+
Returns a base64-encoded @p NSString* of specified number of random bytes.
483+
484+
@param length
485+
A reasonably large, non-zero number representing the length in bytes.
486+
For example, a value of 32 would generate a 256-bit token.
487+
488+
@result Returns @p NSString* on success; panics on failure.
489+
*/
490+
NSString* generateToken(uint length) {
491+
UInt8 buf[length];
492+
493+
int error = SecRandomCopyBytes(kSecRandomDefault, length, &buf);
494+
if (error != errSecSuccess) {
495+
panic("failed to generate token");
496+
}
497+
498+
return [[NSData dataWithBytes:buf length:length] base64EncodedStringWithOptions:0];
499+
}
500+
447501
#
448502
# pragma mark script support
449503
#

Uebersicht/UBWebSocket.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
@interface UBWebSocket : NSObject <SRWebSocketDelegate>
1313

1414
+ (id)sharedSocket;
15-
- (void)open:(NSURL*)aUrl;
15+
- (void)open:(NSURL*)aUrl
16+
token:(NSString*)aToken;
1617
- (void)close;
1718
- (void)send:(id)message;
1819
- (void)listen:(void (^)(id))listener;

Uebersicht/UBWebSocket.m

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ @implementation UBWebSocket {
1313
NSMutableArray* queuedMessages;
1414
SRWebSocket* ws;
1515
NSURL* url;
16+
NSString *token;
1617
}
1718

1819

@@ -49,16 +50,26 @@ - (void)listen:(void (^)(id))listener
4950
}
5051

5152
- (void)open:(NSURL*)aUrl
53+
token:(NSString*)aToken
5254
{
5355
if (ws) {
5456
return;
5557
}
5658

59+
token = aToken;
5760
url = aUrl;
5861
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
5962
[request setValue:@"Übersicht" forHTTPHeaderField:@"Origin"];
6063
ws = [[SRWebSocket alloc] initWithURLRequest: request];
6164
ws.delegate = self;
65+
ws.requestCookies = @[
66+
[NSHTTPCookie cookieWithProperties:@{
67+
NSHTTPCookieDomain: url.host,
68+
NSHTTPCookiePath: @"/",
69+
NSHTTPCookieName: @"token",
70+
NSHTTPCookieValue: token,
71+
}],
72+
];
6273
[ws open];
6374
}
6475

@@ -76,7 +87,8 @@ - (void)reopen
7687
{
7788
[self close];
7889
if (url) {
79-
[self open:url];
90+
[self open:url
91+
token:token];
8092
}
8193
}
8294

Uebersicht/UBWebViewController.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
- (id)initWithFrame:(NSRect)frame;
1717
- (void)load:(NSURL*)url;
18+
- (void)setToken:(NSString*)token;
1819
- (void)reload;
1920
- (void)redraw;
2021
- (void)destroy;

Uebersicht/UBWebViewController.m

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
@implementation UBWebViewController {
1616
NSURL* url;
17+
NSString *token;
1718
}
1819

1920
@synthesize view;
@@ -44,7 +45,14 @@ - (void)load:(NSURL*)newUrl
4445
default:
4546
break;
4647
}
47-
[(WKWebView*)view loadRequest:[NSURLRequest requestWithURL: url]];
48+
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
49+
[request setValue:@"Übersicht" forHTTPHeaderField:@"Origin"];
50+
[(WKWebView*)view loadRequest:request];
51+
}
52+
53+
- (void)setToken:(NSString*)newToken
54+
{
55+
token = newToken;
4856
}
4957

5058
- (void)reload
@@ -173,15 +181,22 @@ - (void)webView:(WKWebView *)sender
173181
[self handleWebviewLoadError:error];
174182
}
175183

176-
177184
- (void)webView: (WKWebView *)theWebView
178185
decidePolicyForNavigationAction: (WKNavigationAction*)action
179186
decisionHandler: (void (^)(WKNavigationActionPolicy))handler
180187
{
181188
if (!action.targetFrame.mainFrame) {
182189
handler(WKNavigationActionPolicyAllow);
183190
} else if ([action.request.URL isEqual: url]) {
184-
handler(WKNavigationActionPolicyAllow);
191+
NSHTTPCookie *c = [NSHTTPCookie cookieWithProperties:@{
192+
NSHTTPCookieDomain: url.host,
193+
NSHTTPCookiePath: @"/",
194+
NSHTTPCookieName: @"token",
195+
NSHTTPCookieValue: token,
196+
}];
197+
[theWebView.configuration.websiteDataStore.httpCookieStore setCookie:c completionHandler:^{
198+
handler(WKNavigationActionPolicyAllow);
199+
}];
185200
} else if (action.navigationType == WKNavigationTypeLinkActivated) {
186201
[[NSWorkspace sharedWorkspace] openURL:action.request.URL];
187202
handler(WKNavigationActionPolicyCancel);

Uebersicht/UBWindow.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ typedef NS_ENUM(NSInteger, UBWindowType) {
2626

2727
- (id)initWithWindowType:(UBWindowType)type;
2828
- (void)loadUrl:(NSURL*)url;
29+
- (void)setToken:(NSString*)token;
2930
- (void)reload;
3031
- (void)workspaceChanged;
3132
- (void)wallpaperChanged;

Uebersicht/UBWindow.m

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ - (void)loadUrl:(NSURL*)url
6262
[webViewController load:url];
6363
}
6464

65+
- (void)setToken:(NSString*)token
66+
{
67+
[webViewController setToken:token];
68+
}
69+
6570
- (void)reload
6671
{
6772
[webViewController reload];

Uebersicht/UBWindowGroup.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ NS_ASSUME_NONNULL_BEGIN
1818

1919
- (id)initWithInteractionEnabled:(BOOL)interactionEnabled;
2020
- (void)loadUrl:(NSURL*)Url;
21+
- (void)setToken:(NSString*)token;
2122
- (void)reload;
2223
- (void)close;
2324
- (void)setFrame:(NSRect)frame display:(BOOL)flag;

Uebersicht/UBWindowGroup.m

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ - (void)loadUrl:(NSURL*)url
5454
[background loadUrl: url];
5555
}
5656

57+
- (void)setToken:(NSString*)token
58+
{
59+
[foreground setToken:token];
60+
[background setToken:token];
61+
}
62+
5763
- (void)setFrame:(NSRect)frame display:(BOOL)flag
5864
{
5965
[foreground setFrame:frame display:flag];

Uebersicht/UBWindowsController.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ NS_ASSUME_NONNULL_BEGIN
1515

1616
- (void)updateWindows:(NSDictionary*)screens
1717
baseUrl:(NSURL*)baseUrl
18+
token:(NSString*)token
1819
interactionEnabled:(Boolean)interactionEnabled
1920
forceRefresh:(Boolean)forceRefresh;
2021

Uebersicht/UBWindowsController.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ - (id)init
3131

3232
- (void)updateWindows:(NSDictionary*)screens
3333
baseUrl:(NSURL*)baseUrl
34+
token:(NSString*)token
3435
interactionEnabled:(Boolean)interactionEnabled
3536
forceRefresh:(Boolean)forceRefresh
3637
{
@@ -43,6 +44,7 @@ - (void)updateWindows:(NSDictionary*)screens
4344
initWithInteractionEnabled: interactionEnabled
4445
];
4546
[windows setObject:windowGroup forKey:screenId];
47+
[windowGroup setToken:token];
4648
[windowGroup loadUrl: [self screenUrl:screenId baseUrl:baseUrl]];
4749
} else {
4850
windowGroup = windows[screenId];

server/server.coffee

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ try
1111
args = parseArgs process.argv.slice(2)
1212
widgetPath = path.resolve(__dirname, args.d ? args.dir ? './widgets')
1313
port = args.p ? args.port ? 41416
14+
token = "#{args.t}" ? "0987654321" # TODO random generation, check not empty
1415
settingsPath = path.resolve(__dirname, args.s ? args.settings ? './settings')
1516
publicPath = path.resolve(__dirname, './public')
1617
options =
@@ -21,6 +22,7 @@ try
2122
widgetPath,
2223
settingsPath,
2324
publicPath,
25+
token,
2426
options,
2527
-> console.log 'server started on port', port
2628
)

server/src/SharedSocket.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ function handleError(err) {
2727
console.error(err);
2828
}
2929

30-
exports.open = function open(url) {
31-
ws = new WebSocket(url, ['ws'], {origin: 'Übersicht'});
30+
exports.open = function open(url, token) {
31+
ws = new WebSocket(url, ['ws'], {origin: 'Übersicht', headers:{cookie:`token=${token}`}});
3232

3333
if (ws.on) {
3434
ws.on('open', handleWSOpen);
@@ -63,4 +63,3 @@ exports.onOpen = function onOpen(listener) {
6363
exports.send = function send(data) {
6464
ws.send(data);
6565
};
66-

server/src/app.coffee

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ WidgetBundler = require('./WidgetBundler.js')
1111
Settings = require('./Settings')
1212
StateServer = require('./StateServer')
1313
ensureSameOrigin = require('./ensureSameOrigin')
14+
ensureToken = require('./ensureToken')
15+
validateTokenCookie = require('./validateTokenCookie')
1416
disallowIFraming = require('./disallowIFraming')
1517
CommandServer = require('./command_server.coffee')
1618
serveWidgets = require('./serveWidgets')
@@ -24,7 +26,7 @@ resolveWidget = require('./resolveWidget')
2426
dispatchToRemote = require('./dispatch')
2527
listenToRemote = require('./listen')
2628

27-
module.exports = (port, widgetPath, settingsPath, publicPath, options, callback) ->
29+
module.exports = (port, widgetPath, settingsPath, publicPath, token, options, callback) ->
2830
options ||= {}
2931

3032
# global store for app state
@@ -74,6 +76,7 @@ module.exports = (port, widgetPath, settingsPath, publicPath, options, callback)
7476
middleware = connect()
7577
.use(disallowIFraming)
7678
.use(ensureSameOrigin(allowedOrigin))
79+
.use(ensureToken(token))
7780
.use(CommandServer(widgetPath, options.loginShell))
7881
.use(StateServer(store))
7982
.use(serveWidgets(bundler, widgetPath))
@@ -90,9 +93,9 @@ module.exports = (port, widgetPath, settingsPath, publicPath, options, callback)
9093
messageBus = MessageBus(
9194
server: server,
9295
verifyClient: (info) ->
93-
info.origin == allowedOrigin || info.origin == 'Übersicht'
96+
(info.origin == allowedOrigin || info.origin == 'Übersicht') && validateTokenCookie(token, info.req.headers.cookie)
9497
)
95-
sharedSocket.open("ws://#{host}:#{port}")
98+
sharedSocket.open("ws://#{host}:#{port}", token)
9699
callback?()
97100
catch e
98101
server.emit('error', e)

server/src/ensureSameOrigin.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module.exports = function ensureSameOrgin(origin) {
22
const fromSameOrigin = (req) => {
3-
return req.method == 'GET' ||
3+
return req.method === 'GET' ||
44
(req.headers.origin && req.headers.origin === origin);
55
}
66

0 commit comments

Comments
 (0)