Skip to content

Commit

Permalink
feat: Lock down all requests to the app server by default #294
Browse files Browse the repository at this point in the history
  • Loading branch information
lo5 committed Apr 17, 2021
1 parent 7e4a608 commit 8056f11
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 19 deletions.
23 changes: 16 additions & 7 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ const (

// App represents an app
type App struct {
broker *Broker
client *http.Client
mode AppMode // mode
route string // route
addr string // upstream address http://host:port
broker *Broker
client *http.Client
mode AppMode // mode
route string // route
addr string // upstream address http://host:port
keyID string // access key ID
keySecret string // access key secret
}

// Boot represents the initial message sent when a client connects to an app
Expand All @@ -53,13 +55,15 @@ func toAppMode(mode string) AppMode {
return unicastMode
}

func newApp(broker *Broker, mode, route, addr string) *App {
func newApp(broker *Broker, mode, route, addr, keyID, keySecret string) *App {
return &App{
broker,
&http.Client{}, // TODO tune keep-alive and idle timeout
toAppMode(mode),
route,
addr,
keyID,
keySecret,
}
}

Expand All @@ -76,6 +80,8 @@ func (app *App) send(clientID string, session *Session, data []byte) error {
return fmt.Errorf("failed creating request: %v", err)
}

req.SetBasicAuth(app.keyID, app.keySecret)

req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.Header.Set("Wave-Client-ID", clientID)
if session.subject != anon {
Expand All @@ -87,9 +93,12 @@ func (app *App) send(clientID string, session *Session, data []byte) error {

resp, err := app.client.Do(req)
if err != nil {
return fmt.Errorf("failed sending request: %v", err)
return fmt.Errorf("request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("request failed: %s", http.StatusText(resp.StatusCode))
}
if _, err := readWithLimit(resp.Body, 0); err != nil { // apps always return empty plain-text responses.
return fmt.Errorf("failed reading response: %v", err)
}
Expand Down
4 changes: 2 additions & 2 deletions broker.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ func newBroker(site *Site) *Broker {
}
}

func (b *Broker) addApp(mode, route, addr string) {
s := newApp(b, mode, route, addr)
func (b *Broker) addApp(mode, route, addr, keyID, keySecret string) {
s := newApp(b, mode, route, addr, keyID, keySecret)

b.appsMux.Lock()
b.apps[route] = s
Expand Down
8 changes: 5 additions & 3 deletions protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,11 @@ type AppRequest struct {

// RegisterApp represents a request to register an app.
type RegisterApp struct {
Mode string `json:"mode"`
Route string `json:"route"`
Address string `json:"address"`
Mode string `json:"mode"`
Route string `json:"route"`
Address string `json:"address"`
KeyID string `json:"key_id"`
KeySecret string `json:"key_secret"`
}

// UnregisterApp represents a request to unregister an app.
Expand Down
15 changes: 12 additions & 3 deletions protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ Relevant environment variables:
- `WAVE_ADDRESS`: The `protocol://ip:port` of the Wave server as visible from the app server.
- `WAVE_APP_ADDRESS`: The `protocol://ip:port` of the app server as visible from the Wave server.
- `WAVE_APP_MODE`: The sync mode of the app, one of `unicast`, `multicast` or `broadcast`.
- `WAVE_ACCESS_KEY_ID`: The API access key ID, typically a 20-character cryptographically random string.
- `WAVE_ACCESS_KEY_SECRET`: The API access key secret, typically a 40-character cryptographically random string.
- `WAVE_ACCESS_KEY_ID`: The Wave server API access key ID, typically a cryptographically random string.
- `WAVE_ACCESS_KEY_SECRET`: The Wave server API access key secret, typically a cryptographically random string.
- `WAVE_APP_ACCESS_KEY_ID`: The app server API access key ID, typically a cryptographically random string.
- `WAVE_APP_ACCESS_KEY_SECRET`: The app server API access key secret, typically a cryptographically random string.

### Startup

Expand All @@ -46,16 +48,23 @@ On app launch, the app registers itself with the Wave server by sending a `POST`
"register_app": {
"mode": "$WAVE_APP_MODE",
"address": "$WAVE_APP_ADDRESS"
"key_id": "$WAVE_APP_ACCESS_KEY_ID",
"key_secret": "$WAVE_APP_ACCESS_KEY_SECRET",
"route": "/foo",
}
}
```

The `key_id` and `key_secret` are automatically generated at startup if `$WAVE_APP_ACCESS_KEY_ID` or `$WAVE_APP_ACCESS_KEY_SECRET` are empty.

### Accepting requests

The Wave server now starts forwarding browser requests from the Wave server's `/foo` to the app server's `/`. Consequently, the app framework requires exactly one HTTP handler, listening to `POST` requests at `/`.

Before parsing the HTTP request and handing over control to the app, the body of the HTTP request is captured and a plain-text empty-string response is sent to the Wave server. The Wave server ignores responses.
On receiving a request, the app server:
1. Verifies if the credentials in the request's basic-authentication header match `$WAVE_APP_ACCESS_KEY_ID` and `$WAVE_APP_ACCESS_KEY_SECRET`.
2. Captures the headers and body of the HTTP request.
3. Responds with a plain-text empty-string (200 status code). Note that the Wave server ignores responses.

### Processing requests

Expand Down
3 changes: 3 additions & 0 deletions py/h2o_wave/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

import json
import secrets
import warnings
import logging
import os
Expand Down Expand Up @@ -47,6 +48,8 @@ def __init__(self):
self.hub_address = _get_env('ADDRESS', 'http://127.0.0.1:10101')
self.hub_access_key_id: str = _get_env('ACCESS_KEY_ID', 'access_key_id')
self.hub_access_key_secret: str = _get_env('ACCESS_KEY_SECRET', 'access_key_secret')
self.app_access_key_id: str = _get_env('APP_ACCESS_KEY_ID', None) or secrets.token_urlsafe(16)
self.app_access_key_secret: str = _get_env('APP_ACCESS_KEY_SECRET', None) or secrets.token_urlsafe(16)


_config = _Config()
Expand Down
31 changes: 28 additions & 3 deletions py/h2o_wave/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import warnings
import pickle
import traceback
import base64
import binascii
from typing import Dict, Tuple, Callable, Any, Awaitable, Optional
from urllib.parse import urlparse

Expand All @@ -38,8 +40,8 @@
from starlette.responses import PlainTextResponse
from starlette.background import BackgroundTask

from .core import Expando, expando_to_dict, _config, marshal, unmarshal, _content_type_json, AsyncSite, _get_env, \
UNICAST, MULTICAST
from .core import Expando, expando_to_dict, _config, marshal, _content_type_json, AsyncSite, _get_env, UNICAST, \
MULTICAST
from .ui import markdown_card

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -237,7 +239,14 @@ def __init__(self, route: str, handle: HandleAsync, mode=None, on_startup: Optio
async def _register(self):
app_address = _get_env('APP_ADDRESS', _config.app_address)
logger.debug(f'Registering app at {app_address} ...')
await self._wave.call('register_app', mode=self._mode, route=self._route, address=app_address)
await self._wave.call(
'register_app',
mode=self._mode,
route=self._route,
address=app_address,
key_id=_config.app_access_key_id,
key_secret=_config.app_access_key_secret,
)
logger.debug('Register: success!')

async def _unregister(self):
Expand All @@ -246,13 +255,29 @@ async def _unregister(self):
logger.debug('Unregister: success!')

async def _receive(self, req: Request):
basic_auth = req.headers.get("Authorization")
if basic_auth is None:
return PlainTextResponse(content='Unauthorized', status_code=401)
try:
scheme, credentials = basic_auth.split()
if scheme.lower() != 'basic':
return PlainTextResponse(content='Unauthorized', status_code=401)
decoded = base64.b64decode(credentials).decode("ascii")
except (ValueError, UnicodeDecodeError, binascii.Error) as exc:
return PlainTextResponse(content='Unauthorized', status_code=401)

key_id, _, key_secret = decoded.partition(":")
if key_id != _config.app_access_key_id or key_secret != _config.app_access_key_secret:
return PlainTextResponse(content='Unauthorized', status_code=401)

client_id = req.headers.get('Wave-Client-ID')
subject = req.headers.get('Wave-Subject-ID')
username = req.headers.get('Wave-Username')
access_token = req.headers.get('Wave-Access-Token')
refresh_token = req.headers.get('Wave-Refresh-Token')
auth = Auth(username, subject, access_token, refresh_token)
args = await req.json()

return PlainTextResponse('', background=BackgroundTask(self._process, client_id, auth, args))

async def _process(self, client_id: str, auth: Auth, args: dict):
Expand Down
2 changes: 1 addition & 1 deletion web_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func (s *WebServer) post(w http.ResponseWriter, r *http.Request) {
}
if req.RegisterApp != nil {
q := req.RegisterApp
s.broker.addApp(q.Mode, q.Route, q.Address)
s.broker.addApp(q.Mode, q.Route, q.Address, q.KeyID, q.KeySecret)
} else if req.UnregisterApp != nil {
q := req.UnregisterApp
s.broker.dropApp(q.Route)
Expand Down

0 comments on commit 8056f11

Please sign in to comment.