Compare commits

...

25 Commits

Author SHA1 Message Date
Felicia Seo
64ebfbf313 .css, .svg 2022-12-11 20:01:25 -05:00
Felicia Seo
f82dce4835 partner.json 2022-12-11 19:13:04 -05:00
Felicia Seo
269ac0c501 comments 2022-12-11 19:10:09 -05:00
Felicia Seo
f93671ac58 extra cookie 2022-12-11 18:55:53 -05:00
Felicia Seo
152cd33d83 95/95git add http.c socket.c! 2022-12-11 18:51:41 -05:00
Micah Moore
00151f2e18 77/95 2022-12-11 17:12:25 -05:00
Felicia Seo
aeb235246d ~73/95 2022-12-11 12:23:40 -05:00
Felicia Seo
a874b62771 logout, headers 2022-12-10 20:41:03 -05:00
Felicia Seo
4720b6f233 validate_token_exp, private 2022-12-09 17:27:52 -05:00
Micah Moore
af936cb46e vunerability fix in handle_static_asset 2022-12-09 14:38:17 -05:00
Felicia Seo
724afe49c1 post login, post logout, multiclient 2022-12-08 08:48:09 -05:00
Micah Moore
5516fe8910 html5 fallback 2022-12-07 20:20:33 -05:00
Micah Moore
45517a03cf threads in main.c, handling video 2022-12-07 19:10:50 -05:00
Micah Moore
6b84d45674 completed get_handle_login 2022-12-07 18:38:58 -05:00
Micah Moore
7f4a319353 support logout in React app frontend, update to 0.2.1 2022-12-07 18:13:22 -05:00
Micah Moore
991fc01c87 get_handle_login; token field in http_transaction 2022-12-05 20:24:32 -05:00
Micah Moore
bbb018471f added val_api_url and updated handle_api 2022-12-05 18:37:33 -05:00
Godmar Back
ad249906f4 Merge branch 'master' of git.cs.vt.edu:cs3214-staff/pserv 2022-11-17 01:11:40 -05:00
Godmar Back
06153db5c1 handout Fall'22
- added Jansson examples to jwt_demo_hs256
- added cookie property tests
2022-11-17 01:10:53 -05:00
cwshugg
a5525414c8 small fix for a test's feedback 2022-04-28 11:52:38 -04:00
cwshugg
721256a343 added onto JSON-claim-checking test (for /api/login) in order to verify servers correctly send JSON claims on a request to GET /api/login with a valid cookie. Also, modified the range-request-checking function to search for the Accept-Range header ONLY in requests to static files 2022-04-27 21:41:42 -04:00
cwshugg
21327b0f76 tweaked feedback for test_two_connections, test_four_connections, and test_eight_connections, to ensure students know they require HTTP/1.1 persistent connection support 2022-04-27 21:18:03 -04:00
cwshugg
c4c35394f4 fixed various testing issues with 'test_video_range_request', 'test_accept_ranges_header', and added a new test to ensure students are sending the correct Content-Type in responses to /api/login requests ('test_login_content_type') 2022-04-27 18:45:44 -04:00
cwshugg
43dbbe35cf minor bugfix in error handling for video streaming test, and a message printed when '-t' can't find a specified test 2022-04-26 14:36:46 -04:00
Godmar Back
be04f7d339 update to current npm versions (React 18, BS5, etc.) 2022-04-19 09:53:48 -04:00
25 changed files with 23409 additions and 13696 deletions

View File

@ -19,7 +19,7 @@ git clone https://github.com/akheron/jansson.git
git clone https://git@github.com/benmcollins/libjwt.git git clone https://git@github.com/benmcollins/libjwt.git
(cd libjwt; (cd libjwt;
git checkout v1.13.1; git checkout v1.14.0;
autoreconf -fi; autoreconf -fi;
env PKG_CONFIG_PATH=../deps/lib/pkgconfig:${PKG_CONFIG_PATH} ./configure --prefix=${BASE}/deps; env PKG_CONFIG_PATH=../deps/lib/pkgconfig:${PKG_CONFIG_PATH} ./configure --prefix=${BASE}/deps;
make -j 40 install make -j 40 install

21
react-app/.gitignore vendored
View File

@ -1,21 +1,2 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules /node_modules
.gitignore
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

36041
react-app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,24 +1,25 @@
{ {
"name": "react-app", "name": "react-app",
"version": "0.2.0", "version": "0.2.1",
"private": true, "private": true,
"dependencies": { "dependencies": {
"bootstrap": "^4.6.1", "bootstrap": "^5.2.2",
"formik": "^2.2.9", "formik": "^2.2.9",
"prop-types": "^15.7.2", "prop-types": "^15.8.1",
"react": "^17.0.2", "react": "^18.2.0",
"react-dom": "^17.0.2", "react-dom": "^18.2.0",
"react-player": "^2.9.0", "react-player": "^2.10.0",
"react-redux": "^7.2.6", "react-redux": "^8.0.5",
"react-router-dom": "^5.2.1", "react-router": "^6.4.3",
"reactstrap": "^8.10.1", "react-router-dom": "^6.4.3",
"reactstrap": "^9.1.5",
"redux": "^4.1.2", "redux": "^4.1.2",
"redux-thunk": "^2.4.0", "redux-thunk": "^2.4.0",
"superagent": "^6.1.0", "superagent": "^8.0.3",
"toastr": "^2.1.4" "toastr": "^2.1.4"
}, },
"devDependencies": { "devDependencies": {
"react-scripts": "4.0.3" "react-scripts": "^5.0.1"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",

View File

@ -1,7 +1,11 @@
https://reacttraining.com/react-router/web/guides/redux-integration
Updated Spring 2020 to React 16.13.1 and react-redux 7.2.0 Updated Spring 2020 to React 16.13.1 and react-redux 7.2.0
Updated Spring 2021 to React 17.0.2 and latest version of all other packages. Updated Spring 2021 to React 17.0.2 and latest version of all other packages.
- added support for video player - added support for video player
Updated Spring 2022 to React 18.0.0 and latest version of all other packages.
- updated bootstrap to BS5 and react-router v6
https://reacttraining.com/react-router/web/guides/redux-integration

View File

@ -31,3 +31,19 @@ export function checklogin() {
}, },
}); });
} }
export function logout() {
return apiAction({
baseType: 'LOGOUT',
fetch() {
return api.auth.logout();
},
onSuccess(dispatch, data, getState) {
toastr.success(`Success logging out: ${JSON.stringify(data)}`);
},
onError(dispatch, data, getState) {
toastr.success(`Error logging out: ${JSON.stringify(data && data.response && data.response.text)}`);
},
});
}

View File

@ -1,14 +1,23 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { Redirect } from 'react-router-dom'; import { Navigate } from 'react-router-dom';
import store from '../store'; import { useDispatch, useSelector } from 'react-redux';
import { logout } from '../actions/auth.js';
import { isLoaded } from '../util/loadingObject'
const Logout = () => { const Logout = () => {
// normally, we would inform the server just in case. const dispatch = useDispatch();
document.cookie = "auth_token="; const user = useSelector(state => state.auth);
const isAuthenticated = isLoaded(user);
// log out on render, and navigate to root on success
useEffect(() => { useEffect(() => {
store.dispatch({ type: "LOGOUT" }); dispatch(logout());
}, []); }, []);
return (<Redirect to="/" />);
if (isAuthenticated)
return (<i>Logging you out ....</i>);
else
return (<Navigate to="/" />);
}; };
export default Logout; export default Logout;

View File

@ -38,10 +38,10 @@ const DropDowns = (props) => {
<DropdownToggle nav caret> <DropdownToggle nav caret>
{dropdown.label} {dropdown.label}
</DropdownToggle> </DropdownToggle>
<DropdownMenu right> <DropdownMenu end>
{dropdown.entries.map((item) => {dropdown.entries.map((item) =>
<DropdownItem key={item.path}> <DropdownItem key={item.path}>
<NavLink to={item.path} key={item.path} activeClassName="active" tag={RRNavLink}> <NavLink to={item.path} key={item.path} tag={RRNavLink}>
{item.label} {item.label}
</NavLink> </NavLink>
</DropdownItem> </DropdownItem>
@ -70,7 +70,7 @@ const NavBar = (props) => {
<Nav className="mr-auto" navbar> <Nav className="mr-auto" navbar>
{menus.topbar.map((item) => {menus.topbar.map((item) =>
<NavItem key={item.path}> <NavItem key={item.path}>
<NavLink to={item.path} activeClassName="active" tag={RRNavLink}> <NavLink to={item.path} tag={RRNavLink}>
{item.label} {item.label}
</NavLink> </NavLink>
</NavItem> </NavItem>
@ -86,11 +86,11 @@ const NavBar = (props) => {
{isLoaded(user) ? {isLoaded(user) ?
<NavItem> <NavItem>
<NavLink activeClassName="active" tag={RRNavLink} to={props.logoutUrl}>Logout ({user.sub})</NavLink> <NavLink tag={RRNavLink} to={props.logoutUrl}>Logout ({user.sub})</NavLink>
</NavItem> </NavItem>
: :
<NavItem> <NavItem>
<NavLink activeClassName="active" tag={RRNavLink} to={props.loginUrl}>Login</NavLink> <NavLink tag={RRNavLink} to={props.loginUrl}>Login</NavLink>
</NavItem> </NavItem>
} }
</Nav> </Nav>

View File

@ -54,9 +54,10 @@ const LoginForm = (props) => {
<ButtonToolbar> <ButtonToolbar>
<Button <Button
type='submit' type='submit'
bsstyle='success' className="mr-2"> bsstyle='success'>
Submit Submit
</Button> </Button>
&nbsp;
<Button <Button
type="button" type="button"
onClick={handleReset} onClick={handleReset}

View File

@ -34,4 +34,7 @@ const apiPrefix = `${publicUrl}/api`
console.log(`Read configuration. Public_URL: ${publicUrl}`) console.log(`Read configuration. Public_URL: ${publicUrl}`)
// console.log(`apiPrefix: ${apiPrefix}`) // console.log(`apiPrefix: ${apiPrefix}`)
export default { menus, apiPrefix, publicUrl } export default { menus,
branding: "CS3214 Demo App 2022",
apiPrefix, publicUrl
}

View File

@ -9,9 +9,9 @@ import HomePage from '../pages/HomePage';
import PrivatePage from '../pages/PrivatePage'; import PrivatePage from '../pages/PrivatePage';
import PlayerPage from '../pages/PlayerPage'; import PlayerPage from '../pages/PlayerPage';
import { Switch, Route, withRouter } from 'react-router-dom'; import { Routes, Route } from 'react-router-dom';
import config from '../config/'; import config from '../config';
/** AppContainer renders the navigation bar on top and its /** AppContainer renders the navigation bar on top and its
* children in the main part of the page. Its children will * children in the main part of the page. Its children will
@ -19,22 +19,22 @@ import config from '../config/';
*/ */
const AppContainer = (props) => ( const AppContainer = (props) => (
<div> <div>
<TopNavBar branding="CS3214 Demo App 2021" <TopNavBar branding={config.branding}
menus={config.menus} menus={config.menus}
user={props.user} user={props.user}
loginUrl={`/login`} loginUrl={`/login`}
logoutUrl={`/logout`} logoutUrl={`/logout`}
/> />
<div className="container-fluid marketing"> <div className="container-fluid marketing">
<Switch> <Routes>
<Route exact path={`/`} component={HomePage} /> <Route exact path={`/`} element={<HomePage />} />
<Route path={`/logout`} component={Logout} /> <Route path={`/logout`} element={<Logout />} />
<Route path={`/login`} component={LoginPage} /> <Route path={`/login`} element={<LoginPage />} />
<Route path={`/public`} component={PublicPage} /> <Route path={`/public`} element={<PublicPage />} />
<Route path={`/protected`} component={PrivatePage} /> <Route path={`/protected`} element={<PrivatePage />} />
<Route path={`/player`} component={PlayerPage} /> <Route path={`/player`} element={<PlayerPage />} />
<Route component={NotFoundPage} /> <Route element={<NotFoundPage />} />
</Switch> </Routes>
</div> </div>
</div> </div>
); );
@ -51,4 +51,4 @@ function mapDispatchToProps(dispatch) {
}; };
} }
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(AppContainer)); export default connect(mapStateToProps, mapDispatchToProps)(AppContainer);

View File

@ -4,33 +4,27 @@
*/ */
import React from 'react'; import React from 'react';
import { Redirect, withRouter } from 'react-router'; import { Navigate, useLocation } from 'react-router';
import { connect } from 'react-redux'; import { useSelector } from 'react-redux';
import { isLoaded } from '../util/loadingObject'; import { isLoaded } from '../util/loadingObject';
function mapStateToProps(state) {
return {
user: state.auth
};
}
export default function RequireAuthentication(Component) { export default function RequireAuthentication(Component) {
const wrapper = props => { const wrapper = props => {
if (isLoaded(props.user)) { const location = useLocation();
const user = useSelector(state => state.auth);
if (isLoaded(user)) {
return <Component {...props} />; return <Component {...props} />;
} else { } else {
return ( return (
<Redirect <Navigate
to={{ to={`/login`}
pathname: `/login`, state = {{
state: { from: location
from: props.history.location }}
}
}}
/> />
); );
} }
}; };
return withRouter(connect(mapStateToProps)(wrapper)); return wrapper;
} }

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
@ -22,6 +23,7 @@ toastr.options.positionClass = 'toast-bottom-right';
store.dispatch(checklogin()); store.dispatch(checklogin());
const mountPoint = document.getElementById('root'); const mountPoint = document.getElementById('root');
const root = createRoot(mountPoint);
const rootNode = ( const rootNode = (
<Provider store={store}> <Provider store={store}>
<Router basename={config.publicUrl}> <Router basename={config.publicUrl}>
@ -29,6 +31,6 @@ const rootNode = (
</Router> </Router>
</Provider> </Provider>
); );
ReactDOM.render(rootNode, mountPoint); root.render(rootNode);
window.jQuery = jQuery; // for toastr window.jQuery = jQuery; // for toastr

View File

@ -5,9 +5,10 @@ import { Card, CardHeader, CardBody, Container, Row, Col } from 'reactstrap';
import { login } from '../actions/auth.js'; import { login } from '../actions/auth.js';
import { isLoading, isLoaded } from '../util/loadingObject' import { isLoading, isLoaded } from '../util/loadingObject'
import LoginForm from '../components/forms/LoginForm'; import LoginForm from '../components/forms/LoginForm';
import { Redirect } from 'react-router-dom'; import { Navigate, useLocation } from 'react-router-dom';
const LoginPage = (props) => { const LoginPage = () => {
const location = useLocation();
const dispatch = useDispatch(); const dispatch = useDispatch();
function doLogin({username, password}) { function doLogin({username, password}) {
dispatch(login(username, password)); dispatch(login(username, password));
@ -15,9 +16,9 @@ const LoginPage = (props) => {
const user = useSelector(state => state.auth); const user = useSelector(state => state.auth);
const isAuthenticated = isLoaded(user); const isAuthenticated = isLoaded(user);
const { from } = props.location.state || { from: { pathname: "/" } }; const { from } = location.state || { from: { pathname: "/" } };
if (isAuthenticated) { if (isAuthenticated) {
return (<Redirect to={from} />); return (<Navigate to={from} />);
} }
return ( return (
@ -38,4 +39,4 @@ const LoginPage = (props) => {
</Container>); </Container>);
} }
export default LoginPage; export default LoginPage;

View File

@ -14,8 +14,8 @@ const PlayerPage = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
let videos = useSelector((state) => state.video); const videos = useSelector((state) => state.video);
let [state, setState] = useState({ const [state, setState] = useState({
url: "" url: ""
}); });
const handleChange = (event) => { const handleChange = (event) => {
@ -61,7 +61,10 @@ const PlayerPage = () => {
</ButtonToolbar> </ButtonToolbar>
</Col> </Col>
<Col> <Col>
or select one from the list of <FormGroup>
<Label for="ddmenu">
or select one from the list of
</Label>
{!isLoaded(videos) ? ( {!isLoaded(videos) ? (
<Spinner size="lg" color="primary" /> <Spinner size="lg" color="primary" />
) : ( ) : (
@ -79,6 +82,7 @@ const PlayerPage = () => {
</DropdownMenu> </DropdownMenu>
</Dropdown> </Dropdown>
)} )}
</FormGroup>
</Col> </Col>
</Row> </Row>
</Container> </Container>

View File

@ -9,16 +9,19 @@ const PrivatePage = () => {
return (<Container> return (<Container>
<h1>Welcome to a private page</h1> <h1>Welcome to a private page</h1>
<Row className="mt-3"> <Row className="mt-3">
You have successfully authenticated as user&nbsp;<span>{user.sub}</span>. <p>
You have successfully authenticated as user&nbsp;<tt>{user.sub}</tt>.
</p>
</Row> </Row>
<Row className="mt-3"> <Row className="mt-3">
Your token was issued at {new Date(user.iat*1000).toString()}, <p>Your token was issued at {new Date(user.iat*1000).toString()},
it expires {new Date(user.exp*1000).toString()} it expires {new Date(user.exp*1000).toString()}</p>
</Row> </Row>
<Row className="mt-3"> <Row className="mt-3">
This page is "private" only inasmuch as the front-end does not <p> This page is "private" only inasmuch as the front-end does not
display it to unauthenticated users. In a fully-fledged app, display it to unauthenticated users. In a fully-fledged app,
this page would now perform API requests that require authentication. this page would now perform API requests that require authentication.
</p>
</Row> </Row>
</Container>); </Container>);
} }

View File

@ -29,7 +29,7 @@ export default function(state = initialState, action) {
if (!('sub' in newState)) if (!('sub' in newState))
delete newState.loadingStatus; delete newState.loadingStatus;
} }
if (action.type === 'LOGOUT') { if (action.type.startsWith('LOGOUT:')) {
newState = { } newState = { }
} }

View File

@ -19,6 +19,8 @@
#include <time.h> #include <time.h>
#include <fcntl.h> #include <fcntl.h>
#include <linux/limits.h> #include <linux/limits.h>
#include <dirent.h>
#include <jansson.h>
#include "http.h" #include "http.h"
#include "hexdump.h" #include "hexdump.h"
@ -26,6 +28,8 @@
#include "bufio.h" #include "bufio.h"
#include "main.h" #include "main.h"
static const char * NEVER_EMBED_A_SECRET_IN_CODE = "supa secret";
// Need macros here because of the sizeof // Need macros here because of the sizeof
#define CRLF "\r\n" #define CRLF "\r\n"
#define CR "\r" #define CR "\r"
@ -80,6 +84,8 @@ http_parse_request(struct http_transaction *ta)
static bool static bool
http_process_headers(struct http_transaction *ta) http_process_headers(struct http_transaction *ta)
{ {
ta->from = -1;
ta->to = -1;
for (;;) { for (;;) {
size_t header_offset; size_t header_offset;
ssize_t len = bufio_readline(ta->client->bufio, &header_offset); ssize_t len = bufio_readline(ta->client->bufio, &header_offset);
@ -111,10 +117,65 @@ http_process_headers(struct http_transaction *ta)
if (!strcasecmp(field_name, "Content-Length")) { if (!strcasecmp(field_name, "Content-Length")) {
ta->req_content_len = atoi(field_value); ta->req_content_len = atoi(field_value);
} }
ta->valid_token = false;
/* Handle other headers here. Both field_value and field_name /* Handle other headers here. Both field_value and field_name
* are zero-terminated strings. * are zero-terminated strings.
*/ */
// Cookie header
if (!strcasecmp(field_name, "Cookie")) {
// Gets the cookies.
char* cookie2;
char* cookie1 = strtok_r(field_value, ";", &cookie2);
cookie2++;
// Stores the 2nd cookie.
ta->extra_cookie = cookie2;
// Determines if the 1st cookie is invalid.
if ((cookie1 == NULL) || (strlen(cookie1) <= 11))
{
ta->resp_status = HTTP_PERMISSION_DENIED;
return false;
}
// If not, find token.
ta->token = cookie1 + 11; // + 11 gets rid of "auth_token=" heading.
ta->valid_token = validate_token_exp(ta, ta->token);
}
// Range header
if (!strcasecmp(field_name, "Range")){
// Gets token.
char *endp;
char *token = strtok_r(field_value, "= ", &endp);
while (token != NULL && strcasecmp(token, "bytes")) {
token = strtok_r(NULL, "= ", &endp);
}
token = strtok_r(NULL, "= ", &endp);
// Determines if the token exist.
if (token != NULL) {
if (token[0] == '-') {
ta->to = atol(token + 1);
}
else if (token[strlen(token) - 1] == '-') {
token[strlen(token) - 1] = '\0';
ta->from = atol(token);
}
else {
char *to;
char *from = strtok_r(token, "- ", &to);
ta->from = atol(from);
ta->to = atol(to);
}
}
ta->range_request = true;
}
} }
} }
@ -150,7 +211,15 @@ add_content_length(buffer_t *res, size_t len)
static void static void
start_response(struct http_transaction * ta, buffer_t *res) start_response(struct http_transaction * ta, buffer_t *res)
{ {
buffer_appends(res, "HTTP/1.0 "); switch (ta->req_version) {
case HTTP_1_0:
buffer_appends(res, "HTTP/1.0 ");
break;
case HTTP_1_1:
default:
buffer_appends(res, "HTTP/1.1 ");
break;
}
switch (ta->resp_status) { switch (ta->resp_status) {
case HTTP_OK: case HTTP_OK:
@ -273,11 +342,49 @@ guess_mime_type(char *filename)
if (!strcasecmp(suffix, ".js")) if (!strcasecmp(suffix, ".js"))
return "text/javascript"; return "text/javascript";
if (!strcasecmp(suffix, ".css"))
return "text/css";
if (!strcasecmp(suffix, ".svg"))
return "image/svg+xml";
if (!strcasecmp(suffix, ".mp4"))
return "video/mp4";
return "text/plain"; return "text/plain";
} }
/* Handle HTTP transaction for static files. */ /* Check a token is not expired. */
bool validate_token_exp(struct http_transaction *ta, char* token) {
jwt_t* cookie;
// Decode token.
int rc = jwt_decode(&cookie, ta->token, (unsigned char *)NEVER_EMBED_A_SECRET_IN_CODE, strlen(NEVER_EMBED_A_SECRET_IN_CODE));
if (rc)
return false;
// Get claim (formatted grants).
char* grants = jwt_get_grants_json(cookie, NULL);
if (grants == NULL)
return false;
ta->grants = grants;
// Get expiration time.
json_error_t error;
json_t *jgrants = json_loadb(grants, strlen(grants), 0, &error);
json_int_t exp, iat;
const char *sub;
json_unpack(jgrants, "{s:I, s:I, s:s}", "exp", &exp, "iat", &iat, "sub", &sub);
// Check expiration time.
if (time(NULL) >= exp)
return false;
return true;
}
/* Handle HTTP transaction for static files. */
static bool static bool
handle_static_asset(struct http_transaction *ta, char *basedir) handle_static_asset(struct http_transaction *ta, char *basedir)
{ {
@ -287,12 +394,37 @@ handle_static_asset(struct http_transaction *ta, char *basedir)
// The code below is vulnerable to an attack. Can you see // The code below is vulnerable to an attack. Can you see
// which? Fix it to avoid indirect object reference (IDOR) attacks. // which? Fix it to avoid indirect object reference (IDOR) attacks.
snprintf(fname, sizeof fname, "%s%s", basedir, req_path); snprintf(fname, sizeof fname, "%s%s", basedir, req_path);
char *endptr;
char *p = calloc(strlen(fname) + 1, sizeof(char));
memcpy(p, fname, strlen(fname) + 1);
char *dir = strtok_r(p, "/", &endptr);
dir = strtok_r(NULL, "/", &endptr); // initial ".." is okay
while (dir != NULL) {
if (!strcmp(dir, "..")) {
return send_not_found(ta);
}
dir = strtok_r(NULL, "/", &endptr);
}
if (!strcmp(req_path, "/")) {
memset(fname, 0, PATH_MAX);
snprintf(fname, sizeof fname, "%s%s", server_root, "/index.html");
}
if (access(fname, R_OK)) { if (access(fname, R_OK)) {
if (errno == EACCES) if (errno == EACCES)
return send_error(ta, HTTP_PERMISSION_DENIED, "Permission denied."); return send_error(ta, HTTP_PERMISSION_DENIED, "Permission denied.");
else else {
return send_not_found(ta); if (html5_fallback) {
memset(fname, 0, PATH_MAX);
snprintf(fname, sizeof fname, "%s%s", server_root, "/index.html");
} else {
return send_not_found(ta);
}
}
} }
// Determine file size // Determine file size
@ -310,9 +442,28 @@ handle_static_asset(struct http_transaction *ta, char *basedir)
http_add_header(&ta->resp_headers, "Content-Type", "%s", guess_mime_type(fname)); http_add_header(&ta->resp_headers, "Content-Type", "%s", guess_mime_type(fname));
off_t from = 0, to = st.st_size - 1; off_t from = 0, to = st.st_size - 1;
if (ta->range_request) {
if (ta->from >= 0) {
from = ta->from;
}
if (ta->to >= 0) {
to = ta->to;
}
}
off_t content_length = to + 1 - from; off_t content_length = to + 1 - from;
add_content_length(&ta->resp_headers, content_length); add_content_length(&ta->resp_headers, content_length);
http_add_header(&ta->resp_headers, "Accept-Ranges", "bytes");
if (ta->range_request) {
ta->resp_status = HTTP_PARTIAL_CONTENT;
http_add_header(&ta->resp_headers, "Content-Range",
"bytes %ld-%ld/%ld", from, to, st.st_size);
}
bool success = send_response_header(ta); bool success = send_response_header(ta);
if (!success) if (!success)
goto out; goto out;
@ -324,12 +475,209 @@ handle_static_asset(struct http_transaction *ta, char *basedir)
out: out:
close(filefd); close(filefd);
return success; return success;
}
/* Handles api/login POST */
static bool
post_handle_login(struct http_transaction *ta)
{
http_add_header(&ta->resp_headers, "Content-Type", "application/json");
char* req_body = bufio_offset2ptr(ta->client->bufio, ta->req_body);
// Gets the username and password.
req_body[ta->req_content_len] = '\0';
json_error_t error;
json_t *jgrants = json_loadb(req_body, strlen(req_body), 0, &error);
const char *username;
const char *password;
int rc = json_unpack(jgrants, "{s:s, s:s}", "username", &username, "password", &password);
if (rc == -1)
{
return send_error(ta, HTTP_PERMISSION_DENIED, "???");
}
// Authenticates the username and password.
if (!strcmp(username, "user0") && !strcmp(password, "thepassword"))
{
int iat = time(NULL);
int exp = iat + token_expiration_time;
// Create cookie.
jwt_t* cookie;
int rc = jwt_new(&cookie);
if (rc)
return send_error(ta, HTTP_INTERNAL_ERROR, "jwt_new");
// Add grants.
rc = jwt_add_grant(cookie, "sub", username);
if (rc)
return send_error(ta, HTTP_INTERNAL_ERROR, "jwt_add_grant");
//time_t age = time(NULL);
rc = jwt_add_grant_int(cookie, "iat", iat);
if (rc)
return send_error(ta, HTTP_INTERNAL_ERROR, "jwt_add_grant");
//time_t max_age = age + 3600 * 24;
rc = jwt_add_grant_int(cookie, "exp", exp);
if (rc)
return send_error(ta, HTTP_INTERNAL_ERROR, "jwt_add_grant");
// Set algorithm.
rc = jwt_set_alg(cookie, JWT_ALG_HS256, (unsigned char *)NEVER_EMBED_A_SECRET_IN_CODE, strlen(NEVER_EMBED_A_SECRET_IN_CODE));
if (rc)
return send_error(ta, HTTP_INTERNAL_ERROR, "jwt_set_alg");
// Create token.
char* token = jwt_encode_str(cookie);
if (token == NULL)
return send_error(ta, HTTP_INTERNAL_ERROR, "jwt_encode_str");
// Add Set-Cookie header.
http_add_header(&ta->resp_headers, "Set-Cookie", "auth_token=%s; Path=%s; Max-Age=%d; HttpOnly", token, "/", token_expiration_time);
// Get claim (formatted grants).
char *grants = jwt_get_grants_json(cookie, NULL);
buffer_appends(&ta->resp_body, grants);
// Send claim.
ta->resp_status = HTTP_OK;
return send_response(ta);
}
return send_error(ta, HTTP_PERMISSION_DENIED, "Permission denied");
} }
/* Handles api/login GET */
static bool get_handle_login(struct http_transaction *ta) {
http_add_header(&ta->resp_headers, "Content-Type", "application/json");
// Determines if the token is invalid.
if (!ta->valid_token) {
buffer_appends(&ta->resp_body, "{}");
ta->resp_status = HTTP_OK;
return send_response(ta);
}
// If not, send response.
buffer_appends(&ta->resp_body, ta->grants);
ta->resp_status = HTTP_OK;
return send_response(ta);
}
/* Lists the .mp4 files in the given directory in the proper format. */
static char *list_videos(DIR* dir) {
json_t *list = json_array();
struct dirent *file;
while ((file = readdir(dir)) != NULL) {
char *suffix = strrchr(file->d_name, '.');
if (suffix != NULL && !strcmp(suffix, ".mp4")) {
// stat the file
char fname[PATH_MAX];
struct stat st;
snprintf(fname, sizeof(fname), "%s/%s", server_root, file->d_name);
int rc = stat(fname, &st);
if (rc == -1)
perror("Could not list videos.");
// json components
json_t *entry = json_object();
json_t *size = json_integer(st.st_size);
json_t *name = json_string(file->d_name);
rc = json_object_set(entry, "size", size);
if (rc)
perror("JSON append failed.");
rc = json_object_set(entry, "name", name);
if (rc)
perror("JSON append failed.");
// add entry
rc = json_array_append_new(list, entry);
if (rc)
perror("JSON append failed.");
}
}
return json_dumps(list, 0);
}
/* Determines what the API request is. */
static int val_api_url(struct http_transaction *ta) {
char *req_path = bufio_offset2ptr(ta->client->bufio, ta->req_path);
if (!strcmp(req_path, "/api/login")) {
return 0;
}
if (!strcmp(req_path, "/api/video")) {
return 1;
}
if (!strcmp(req_path, "/api/logout")) {
return 2;
}
return -1;
}
/* Handles API requests like login/logout/video. */
static bool static bool
handle_api(struct http_transaction *ta) handle_api(struct http_transaction *ta)
{ {
return send_error(ta, HTTP_NOT_FOUND, "API not implemented"); int val = val_api_url(ta);
if (val == -1){
return send_not_found(ta);
}
// API login
if (val == 0) {
if (ta->req_method == HTTP_POST) {
// Handle login post
return post_handle_login(ta);
}
else if (ta->req_method == HTTP_GET){
// Handle login get
return get_handle_login(ta);
}
else{
return send_error(ta, HTTP_METHOD_NOT_ALLOWED, "Unknown method.\n");
}
}
// API video
if (val == 1) {
http_add_header(&ta->resp_headers, "Accept-Ranges", "bytes");
DIR* dir = opendir(server_root);
char *json = list_videos(dir); // handling
fprintf(stderr, "json: %s\n", json);
http_add_header(&ta->resp_headers, "Content-Type", "application/json");
// Ensures response can be sent and sends it.
char *body = buffer_ensure_capacity(&ta->resp_body, MAX_HEADER_LEN);
int len = snprintf(body, strlen(json) + 2, "%s\n", json);
int length = len > MAX_HEADER_LEN ? MAX_HEADER_LEN - 1 : len;
ta->resp_body.len += length;
ta->resp_status = HTTP_OK;
return send_response(ta);
}
// API logout
if (val == 2)
{
// Add Set-Cookie header.
http_add_header(&ta->resp_headers, "Set-Cookie", "auth_token=; Path=/; Max-Age=0; HttpOnly");
// Send message.
buffer_appends(&ta->resp_body, "{\"message\":\"logging out\"}");
ta->resp_status = HTTP_OK;
return send_response(ta);
}
return false;
} }
/* Set up an http client, associating it with a bufio buffer. */ /* Set up an http client, associating it with a bufio buffer. */
@ -339,6 +687,35 @@ http_setup_client(struct http_client *self, struct bufio *bufio)
self->bufio = bufio; self->bufio = bufio;
} }
/* Handles attempts to access private files. */
static bool
handle_private(struct http_transaction *ta)
{
// Determines if the token from the 1st cookie is valid.
if (ta->valid_token) {
ta->resp_status = HTTP_OK;
return handle_static_asset(ta, server_root);
}
// Determines if there's a 2nd cookie.
if ((ta->extra_cookie == NULL) || (strlen(ta->extra_cookie) <= 11)) {
return send_error(ta, HTTP_PERMISSION_DENIED, "Permission denied.");
}
// If so, find its token.
ta->token = ta->extra_cookie + 11; // + 11 gets rid of "auth_token=" heading.
ta->valid_token = validate_token_exp(ta, ta->token);
// valid
if (ta->valid_token) {
ta->resp_status = HTTP_OK;
return handle_static_asset(ta, server_root);
}
// invalid
return send_error(ta, HTTP_PERMISSION_DENIED, "Permission denied.");
}
/* Handle a single HTTP transaction. Returns true on success. */ /* Handle a single HTTP transaction. Returns true on success. */
bool bool
http_handle_transaction(struct http_client *self) http_handle_transaction(struct http_client *self)
@ -349,10 +726,14 @@ http_handle_transaction(struct http_client *self)
if (!http_parse_request(&ta)) if (!http_parse_request(&ta))
return false; return false;
bool is_http_1_1 = false; // false
if (ta.req_version == HTTP_1_1)
is_http_1_1 = true; // true
if (!http_process_headers(&ta)) if (!http_process_headers(&ta))
return false; return false;
if (ta.req_content_len > 0) { if (ta.req_content_len > 0) {
int rc = bufio_read(self->bufio, ta.req_content_len, &ta.req_body); int rc = bufio_read(self->bufio, ta.req_content_len, &ta.req_body);
if (rc != ta.req_content_len) if (rc != ta.req_content_len)
@ -373,7 +754,7 @@ http_handle_transaction(struct http_client *self)
rc = handle_api(&ta); rc = handle_api(&ta);
} else } else
if (STARTS_WITH(req_path, "/private")) { if (STARTS_WITH(req_path, "/private")) {
/* not implemented */ rc = handle_private(&ta);
} else { } else {
rc = handle_static_asset(&ta, server_root); rc = handle_static_asset(&ta, server_root);
} }
@ -381,5 +762,5 @@ http_handle_transaction(struct http_client *self)
buffer_delete(&ta.resp_headers); buffer_delete(&ta.resp_headers);
buffer_delete(&ta.resp_body); buffer_delete(&ta.resp_body);
return rc; return is_http_1_1 && rc;
} }

View File

@ -39,7 +39,12 @@ struct http_transaction {
size_t req_path; // expressed as offset into the client's bufio. size_t req_path; // expressed as offset into the client's bufio.
size_t req_body; // ditto size_t req_body; // ditto
int req_content_len; // content length of request body int req_content_len; // content length of request body
char* extra_cookie; // the 2nd cookie if it exists.
char *token; // authentication token
bool valid_token; // true if valid, false if not.
char* grants; // exp, iat, sub
/* response related fields */ /* response related fields */
enum http_response_status resp_status; enum http_response_status resp_status;
@ -47,6 +52,10 @@ struct http_transaction {
buffer_t resp_body; buffer_t resp_body;
struct http_client *client; struct http_client *client;
off_t from; // range from
off_t to; // range to
bool range_request;
}; };
struct http_client { struct http_client {
@ -56,5 +65,6 @@ struct http_client {
void http_setup_client(struct http_client *, struct bufio *bufio); void http_setup_client(struct http_client *, struct bufio *bufio);
bool http_handle_transaction(struct http_client *); bool http_handle_transaction(struct http_client *);
void http_add_header(buffer_t * resp, char* key, char* fmt, ...); void http_add_header(buffer_t * resp, char* key, char* fmt, ...);
bool validate_token_exp(struct http_transaction *ta, char* token);
#endif /* _HTTP_H */ #endif /* _HTTP_H */

View File

@ -2,6 +2,10 @@
* Quick demo of how to use libjwt using a HS256. * Quick demo of how to use libjwt using a HS256.
* *
* @author gback, CS 3214, Spring 2018, updated Spring 2021 * @author gback, CS 3214, Spring 2018, updated Spring 2021
* Added Jansson demo Fall'22
*
* I included the necessary free() operations here which
* are needed in a long-running server.
*/ */
#include <jwt.h> #include <jwt.h>
#include <stdlib.h> #include <stdlib.h>
@ -9,6 +13,7 @@
#include <string.h> #include <string.h>
#include <time.h> #include <time.h>
#include <errno.h> #include <errno.h>
#include <jansson.h>
static const char * NEVER_EMBED_A_SECRET_IN_CODE = "supa secret"; static const char * NEVER_EMBED_A_SECRET_IN_CODE = "supa secret";
@ -56,6 +61,7 @@ main()
if (encoded == NULL) if (encoded == NULL)
die("jwt_encode_str", ENOMEM); die("jwt_encode_str", ENOMEM);
jwt_free(mytoken);
printf("encoded as %s\nTry entering this at jwt.io\n", encoded); printf("encoded as %s\nTry entering this at jwt.io\n", encoded);
jwt_t *ymtoken; jwt_t *ymtoken;
@ -65,9 +71,32 @@ main()
if (rc) if (rc)
die("jwt_decode", rc); die("jwt_decode", rc);
free(encoded);
char *grants = jwt_get_grants_json(ymtoken, NULL); // NULL means all char *grants = jwt_get_grants_json(ymtoken, NULL); // NULL means all
if (grants == NULL) if (grants == NULL)
die("jwt_get_grants_json", ENOMEM); die("jwt_get_grants_json", ENOMEM);
jwt_free(ymtoken);
printf("redecoded: %s\n", grants); printf("redecoded: %s\n", grants);
// an example of how to use Jansson
json_error_t error;
json_t *jgrants = json_loadb(grants, strlen(grants), 0, &error);
if (jgrants == NULL)
die("json_loadb", EINVAL);
free (grants);
json_int_t exp, iat;
const char *sub;
rc = json_unpack(jgrants, "{s:I, s:I, s:s}",
"exp", &exp, "iat", &iat, "sub", &sub);
if (rc == -1)
die("json_unpack", EINVAL);
printf ("exp: %lld\n", exp);
printf ("iat: %lld\n", iat);
printf ("sub: %s\n", sub);
json_decref(jgrants);
} }

View File

@ -9,6 +9,7 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <signal.h> #include <signal.h>
#include <pthread.h>
#include "buffer.h" #include "buffer.h"
#include "hexdump.h" #include "hexdump.h"
#include "http.h" #include "http.h"
@ -34,6 +35,19 @@ int token_expiration_time = 24 * 60 * 60;
// root from which static files are served // root from which static files are served
char * server_root; char * server_root;
static void *thr_func(void *c) {
struct http_client *client = c;
while (http_handle_transaction(client)){};
bufio_close(client->bufio);
return NULL;
}
/* /*
* A non-concurrent, iterative server that serves one client at a time. * A non-concurrent, iterative server that serves one client at a time.
* For each client, it handles exactly 1 HTTP transaction. * For each client, it handles exactly 1 HTTP transaction.
@ -41,18 +55,35 @@ char * server_root;
static void static void
server_loop(char *port_string) server_loop(char *port_string)
{ {
int max_clients = 8;
int num_clients = 0;
pthread_t *threads = malloc(max_clients * sizeof(pthread_t));
int accepting_socket = socket_open_bind_listen(port_string, 10000); int accepting_socket = socket_open_bind_listen(port_string, 10000);
while (accepting_socket != -1) { while (accepting_socket != -1) {
fprintf(stderr, "Waiting for client...\n"); fprintf(stderr, "Waiting for client...\n");
int client_socket = socket_accept_client(accepting_socket); int client_socket = socket_accept_client(accepting_socket);
if (client_socket == -1)
return;
struct http_client client; if (client_socket == -1){
http_setup_client(&client, bufio_create(client_socket)); return;
http_handle_transaction(&client); }
bufio_close(client.bufio); struct http_client *client = malloc(sizeof(struct http_client));
http_setup_client(client, bufio_create(client_socket));
int rc = pthread_create(&threads[num_clients++], NULL, thr_func, client);
if (rc) {
perror(NULL);
} else if (num_clients >= max_clients) {
max_clients *= 2;
threads = realloc(threads, max_clients * sizeof(pthread_t));
}
} }
for (int i = 0; i < num_clients; i++) {
pthread_join(threads[i], NULL);
}
} }
static void static void
@ -62,6 +93,7 @@ usage(char * av0)
" -p port port number to bind to\n" " -p port port number to bind to\n"
" -R rootdir root directory from which to serve files\n" " -R rootdir root directory from which to serve files\n"
" -e seconds expiration time for tokens in seconds\n" " -e seconds expiration time for tokens in seconds\n"
" -a enable HTML5 fallback\n"
" -h display this help\n" " -h display this help\n"
, av0); , av0);
exit(EXIT_FAILURE); exit(EXIT_FAILURE);

1
src/partner.json Normal file
View File

@ -0,0 +1 @@
["seofelicia", "micahmoore"]

View File

@ -54,6 +54,57 @@ socket_open_bind_listen(char * port_number_string, int backlog)
} }
char printed_addr[1024]; char printed_addr[1024];
for (pinfo = info; pinfo; pinfo = pinfo->ai_next) {
assert (pinfo->ai_protocol == IPPROTO_TCP);
int rc = getnameinfo(pinfo->ai_addr, pinfo->ai_addrlen,
printed_addr, sizeof printed_addr, NULL, 0,
NI_NUMERICHOST);
if (rc != 0) {
fprintf(stderr, "getnameinfo error: %s\n", gai_strerror(rc));
return -1;
}
/* Uncomment this to see the address returned
printf("%s: %s\n", pinfo->ai_family == AF_INET ? "AF_INET" :
pinfo->ai_family == AF_INET6 ? "AF_INET6" : "?",
printed_addr);
*/
/* Skip any non-IPv4 addresses.
* Adding support for protocol independence/IPv6 is part of the project.
*/
if (pinfo->ai_family != AF_INET6)
continue;
int s = socket(pinfo->ai_family, pinfo->ai_socktype, pinfo->ai_protocol);
if (s == -1) {
perror("socket");
return -1;
}
// See https://stackoverflow.com/a/3233022 for a good explanation of what this does
int opt = 1;
setsockopt (s, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (opt));
rc = bind(s, pinfo->ai_addr, pinfo->ai_addrlen);
if (rc == -1) {
perror("bind");
close(s);
return -1;
}
rc = listen(s, backlog);
if (rc == -1) {
perror("listen");
close(s);
return -1;
}
freeaddrinfo(info);
return s;
}
for (pinfo = info; pinfo; pinfo = pinfo->ai_next) { for (pinfo = info; pinfo; pinfo = pinfo->ai_next) {
assert (pinfo->ai_protocol == IPPROTO_TCP); assert (pinfo->ai_protocol == IPPROTO_TCP);
int rc = getnameinfo(pinfo->ai_addr, pinfo->ai_addrlen, int rc = getnameinfo(pinfo->ai_addr, pinfo->ai_addrlen,

View File

@ -4,7 +4,7 @@ PORT=10000
# to test against a working implementation (and see the intended responses) # to test against a working implementation (and see the intended responses)
# change this variable, e.g. # change this variable, e.g.
# use URL=http://theta.cs.vt.edu:3000/ #URL=http://hazelnut.rlogin:12345
URL=http://localhost:${PORT} URL=http://localhost:${PORT}
# the file in which curl stores cookies across runs # the file in which curl stores cookies across runs
@ -34,8 +34,17 @@ curl -v \
curl -v \ curl -v \
${URL}/private/secret.txt ${URL}/private/secret.txt
# this should succeed since credentials are included # this should succeed since credentials are included (via the cookie jar)
curl -v \ curl -v \
-b ${COOKIEJAR} \ -b ${COOKIEJAR} \
${URL}/private/secret.txt ${URL}/private/secret.txt
# now log out
curl -v -X POST \
-c ${COOKIEJAR} \
${URL}/api/logout
# this should fail since the cookie should have been removed from the cookie jar
curl -v \
-b ${COOKIEJAR} \
${URL}/private/secret.txt

View File

@ -61,27 +61,6 @@ def get_socket_connection(hostname, port):
else: else:
return sock return sock
def run_connection_check_loadavg(http_conn, hostname):
"""
Run a check of the connection for validity, using a well-formed
request for /loadavg and checking it after receiving it.
"""
# GET request for the object /loadavg
http_conn.request("GET", "/loadavg", headers={"Host": hostname})
# Get the server's response
server_response = http_conn.getresponse()
# Check the response status code
assert server_response.status == OK, "Server failed to respond"
# Check the data included in the server's response
assert check_loadavg_response(server_response.read().decode('utf-8')), \
"loadavg check failed"
def run_connection_check_empty_login(http_conn, hostname): def run_connection_check_empty_login(http_conn, hostname):
""" """
Run a check of the connection for validity, using a well-formed Run a check of the connection for validity, using a well-formed
@ -95,7 +74,9 @@ def run_connection_check_empty_login(http_conn, hostname):
server_response = http_conn.getresponse() server_response = http_conn.getresponse()
# Check the response status code # Check the response status code
assert server_response.status == OK, "Server failed to respond" assert server_response.status == OK, "Server failed to respond. " \
"This test will fail until persistent connections are implemented (i.e. HTTP/1.1 support). " \
"We recommend you implement this before moving forward."
# Check the data included in the server's response # Check the data included in the server's response
assert check_empty_login_respnse(server_response.read().decode('utf-8')), \ assert check_empty_login_respnse(server_response.read().decode('utf-8')), \
@ -107,7 +88,7 @@ def run_404_check(http_conn, obj, hostname):
requesting a non-existent URL object. requesting a non-existent URL object.
""" """
# GET request for the object /loadavg # GET request for obj
http_conn.request("GET", obj, headers={"Host": hostname}) http_conn.request("GET", obj, headers={"Host": hostname})
# Get the server's response # Get the server's response
@ -119,27 +100,6 @@ def run_404_check(http_conn, obj, hostname):
server_response.read() server_response.read()
def run_query_check(http_conn, request, req_object, callback, hostname):
"""
Checks that the server properly processes the query string passed to it.
"""
http_conn.request("GET", request, headers={"Host": hostname})
server_response = http_conn.getresponse()
assert server_response.status == OK, "Server failed to respond"
if callback is None:
if req_object == "loadavg":
assert check_loadavg_response(server_response.read().decode('utf-8')), \
"loadavg check failed"
else:
assert check_meminfo_response(server_response.read().decode('utf-8')), \
"meminfo check failed"
else:
assert check_callback_response(server_response.read().decode('utf-8'),
callback, req_object), "callback check failed"
def run_method_check(http_conn, method, hostname): def run_method_check(http_conn, method, hostname):
""" """
Check that the unsupported method supplied has either a NOT IMPLEMENTED Check that the unsupported method supplied has either a NOT IMPLEMENTED
@ -165,92 +125,6 @@ def print_response(response):
for line in lines: for line in lines:
print(line.strip()) print(line.strip())
def check_loadavg_response(response):
"""Check that the response to a loadavg request generated the correctly
formatted output. Returns true if it executes properly, throws an
AssertionError if it does not execute properly or another error if json
is unable to decode the response."""
try:
data = json.loads(response.strip())
except ValueError as msg:
raise AssertionError("Invalid JSON object. Received: " + response)
assert len(data) == 3, "Improper number of data items returned"
assert 'total_threads' in data, "total_threads element missing"
assert 'loadavg' in data, "loadavg element missing"
assert 'running_threads' in data, "running_threads element missing"
assert len(data['loadavg']) == 3, 'Improper number of data items in \
loadavg'
return True
def check_meminfo_response(response):
"""Check that the response to a meminfo request generated the correctly
formatted output. Returns true if it executes properly, throws an
AssertionError if it does not execute properly or another error if json
is unable to decode the response."""
try:
data = json.loads(response.strip())
except ValueError as msg:
raise AssertionError("Invalid JSON object. Received: " + response)
for line in open("/proc/meminfo"):
entry = re.split(":?\s+", line)
assert entry[0] in data, entry[0] + " key is missing"
try:
int(data[entry[0]])
except (TypeError, ValueError):
raise AssertionError("a non-integer was passed to meminfo")
return True
def check_callback_response(response, callback, req_obj):
"""Check that the response to a req_obj request with callback function
callback generated the correctly formatted output. Returns true if it
executes properly, throws an AssertionError if it does not execute properly
or another error if json is unable to decode the response."""
callback.replace(' ', '')
response.replace(' ', '')
assert response[0:len(callback) + 1] == callback + "(", 'callback incorrect, was: ' + response[0:len(
callback) + 1] + ' , expected: ' + callback + '('
assert response[len(response) - 1] == ")", 'missing close parenthesis'
if req_obj == "meminfo":
check_meminfo_response(response[len(callback) + 1:len(response) - 1])
elif req_obj == "loadavg":
check_loadavg_response(response[len(callback) + 1:len(response) - 1])
else:
return False
return True
def check_meminfo_change(response1, response2, key, delta, safety_margin=0):
"""Check that a specific value in meminfo has changed by at least some amount.
Used by allocanon and freeanon tests. Returns true if it executes properly,
throws an AssertionError if it does not execute properly or another error
if json is unable to decode the response."""
check_meminfo_response(response1)
check_meminfo_response(response2)
data1 = json.loads(response1.strip())
data2 = json.loads(response2.strip())
if delta >= 0:
return float(data2[key]) - float(data1[key]) > delta * (1 - safety_margin)
else:
return float(data2[key]) - float(data1[key]) < delta * (1 - safety_margin)
def check_empty_login_respnse(response): def check_empty_login_respnse(response):
return response.strip() == "{}" return response.strip() == "{}"
@ -385,7 +259,7 @@ class Single_Conn_Protocol_Case(Doc_Print_Test_Case):
pass pass
sock.send(encode("\r\n")) sock.send(encode("\r\n"))
# If there is a HTTP response, it should be a valid /loadavg # If there is a HTTP response, it should be a valid /login
# response. # response.
data = "" data = ""
@ -1196,7 +1070,8 @@ class Multi_Conn_Sequential_Case(Doc_Print_Test_Case):
""" Test Name: test_two_connections\n\ """ Test Name: test_two_connections\n\
Number Connections: 2 \n\ Number Connections: 2 \n\
Procedure: Run 2 connections simultaneously for simple GET requests:\n\ Procedure: Run 2 connections simultaneously for simple GET requests:\n\
GET /api/login HTTP/1.1 GET /api/login HTTP/1.1
NOTE: this test requires HTTP/1.1 persistent connection support.
""" """
# Append two connections to the list # Append two connections to the list
@ -1220,10 +1095,11 @@ class Multi_Conn_Sequential_Case(Doc_Print_Test_Case):
def test_four_connections(self): def test_four_connections(self):
""" Test Name: test_two_connections\n\ """ Test Name: test_four_connections\n\
Number Connections: 4 \n\ Number Connections: 4 \n\
Procedure: Run 4 connections simultaneously for simple GET requests:\n\ Procedure: Run 4 connections simultaneously for simple GET requests:\n\
GET /api/login HTTP/1.1 GET /api/login HTTP/1.1
NOTE: this test requires HTTP/1.1 persistent connection support.
""" """
# Append four connections to the list # Append four connections to the list
@ -1243,10 +1119,11 @@ class Multi_Conn_Sequential_Case(Doc_Print_Test_Case):
run_connection_check_empty_login(http_conn, self.hostname) run_connection_check_empty_login(http_conn, self.hostname)
def test_eight_connections(self): def test_eight_connections(self):
""" Test Name: test_two_connections\n\ """ Test Name: test_eight_connections\n\
Number Connections: 8 \n\ Number Connections: 8 \n\
Procedure: Run 8 connections simultaneously for simple GET requests:\n\ Procedure: Run 8 connections simultaneously for simple GET requests:\n\
GET /api/login HTTP/1.1 GET /api/login HTTP/1.1
NOTE: this test requires HTTP/1.1 persistent connection support.
""" """
# Append eight connections to the list # Append eight connections to the list
@ -1313,13 +1190,13 @@ class Single_Conn_Good_Case(Doc_Print_Test_Case):
print("The server has crashed. Please investigate.") print("The server has crashed. Please investigate.")
def test_login_get(self): def test_login_get(self):
""" Test Name: test_loadavg_no_callback\n\ """ Test Name: test_login_get\n\
Number Connections: One \n\ Number Connections: One \n\
Procedure: Simple GET request:\n\ Procedure: Simple GET request:\n\
GET /api/login HTTP/1.1 GET /api/login HTTP/1.1
""" """
# GET request for the object /loadavg # GET request for the object /api/login
self.http_connection.request("GET", "/api/login") self.http_connection.request("GET", "/api/login")
# Get the server's response # Get the server's response
@ -1375,6 +1252,17 @@ class Access_Control(Doc_Print_Test_Case):
# Close the HTTP connection # Close the HTTP connection
self.session.close() self.session.close()
# =============================== Helpers ================================ #
# Does a lower-case search for headers within a response's headers. If
# found, the first ocurrence is returned (the header's value is returned).
def find_header(self, response, name):
for header in response.headers:
if header.lower() == name.lower():
return response.headers[header]
return None
# ================================ Tests ================================= #
def test_access_control_private_valid_token(self): def test_access_control_private_valid_token(self):
""" Test Name: test_access_control_private_valid_token """ Test Name: test_access_control_private_valid_token
Number Connections: N/A Number Connections: N/A
@ -1690,6 +1578,56 @@ class Access_Control(Doc_Print_Test_Case):
self.assertEqual(response.status_code, requests.codes.not_found, self.assertEqual(response.status_code, requests.codes.not_found,
"Server did not respond with 404 when it should have, possible IDOR?") "Server did not respond with 404 when it should have, possible IDOR?")
def test_login_content_type(self):
""" Test Name: test_login_content_type
Number Connections: N/A
Procedure: Checks to ensure the Content-Type header is being sent in
responses to GETs and POSTs to /api/login (both with AND
without Cookie headers). A failure here means either:
- 'Content-Type' is not a part of some or all of your /api/login responses, OR
- The value of your 'Content-Type' header is not what it should be.
"""
# inner helper function that takes a response and checks for the correct
# content-type header
def check_content_type(response):
# search for the content-type header and ensure we see "application/json"
content_type = self.find_header(response, "Content-Type")
content_expect = "application/json"
if content_type == None:
raise AssertionError("Server didn't respond with the Content-Type header when sent a request to /api/login")
if content_type.lower() != content_expect:
raise AssertionError("Server didn't respond with the correct Content-Type value when sent a request to /api/login. "
"Expected: '%s', received: '%s'" % (content_expect, content_type))
# first, we'll build the /api/login url
login_url = "http://%s:%s/api/login" % (self.hostname, self.port)
# TEST 1: send a simple GET /api/login with NO COOKIE
try:
response = self.session.get(login_url, timeout=2)
check_content_type(response)
except requests.exceptions.RequestException:
raise AssertionError("The server did not respond within 2s")
# TEST 2: try sending a POST /api/login with the correct credentials
try:
response = self.session.post(login_url,
json={'username': self.username, 'password': self.password},
timeout=2)
check_content_type(response)
# Ensure that the user is authenticated
self.assertEqual(response.status_code, requests.codes.ok, "Authentication failed.")
except requests.exceptions.RequestException:
raise AssertionError("The server did not respond within 2s")
# TEST 3: send one more GET /api/login with the cookie we just received
try:
response = self.session.get(login_url, timeout=2)
check_content_type(response)
except requests.exceptions.RequestException:
raise AssertionError("The server did not respond within 2s")
class Fallback(Doc_Print_Test_Case): class Fallback(Doc_Print_Test_Case):
""" """
Test cases for HTML 5 fallback, using good requests that expect a Test cases for HTML 5 fallback, using good requests that expect a
@ -1933,7 +1871,7 @@ class Authentication(Doc_Print_Test_Case):
pool = ThreadPool(30) pool = ThreadPool(30)
pool.map(test_expiry_authentication, range(30)) pool.map(test_expiry_authentication, range(30))
pool.terminate() pool.terminate()
def test_jwt_claims_json(self): def test_jwt_claims_json(self):
""" Test Name: test_jwt_claims_json """ Test Name: test_jwt_claims_json
Number Connections: N/A Number Connections: N/A
@ -1946,6 +1884,7 @@ class Authentication(Doc_Print_Test_Case):
self.sessions.append(requests.Session()) self.sessions.append(requests.Session())
for i in range(30): for i in range(30):
# ----------------------- Login JSON Check ----------------------- #
# Login using the default credentials # Login using the default credentials
try: try:
response = self.sessions[i].post('http://%s:%s/api/login' % (self.hostname, self.port), response = self.sessions[i].post('http://%s:%s/api/login' % (self.hostname, self.port),
@ -1961,19 +1900,13 @@ class Authentication(Doc_Print_Test_Case):
# Convert the response to JSON # Convert the response to JSON
data = response.json() data = response.json()
# Verify that the JWT contains 'iat' # ensure all expected fields are present
assert 'iat' in data, "Could not find the claim 'iat' in the JSON object." assert 'iat' in data, "Could not find the claim 'iat' in the JSON object."
# Verify that the JWT contains 'iat'
assert 'exp' in data, "Could not find the claim 'exp' in the JSON object." assert 'exp' in data, "Could not find the claim 'exp' in the JSON object."
# Verify that the JWT contains 'sub'
assert 'sub' in data, "Could not find the claim 'sub' in the JSON object." assert 'sub' in data, "Could not find the claim 'sub' in the JSON object."
# Verify that the 'iat' claim to is a valid date from self.current_year # verify that the two timestamps are valid dates
assert datetime.fromtimestamp(data['iat']).year == self.current_year, "'iat' returned is not a valid date" assert datetime.fromtimestamp(data['iat']).year == self.current_year, "'iat' returned is not a valid date"
# Verify that the 'exp' claim to is a valid date from self.current_year
assert datetime.fromtimestamp(data['exp']).year == self.current_year, "'exp' returned is not a valid date" assert datetime.fromtimestamp(data['exp']).year == self.current_year, "'exp' returned is not a valid date"
# Verify that the subject claim to is set to the right username # Verify that the subject claim to is set to the right username
@ -1982,6 +1915,34 @@ class Authentication(Doc_Print_Test_Case):
except ValueError: except ValueError:
raise AssertionError('The login API did not return a valid JSON object') raise AssertionError('The login API did not return a valid JSON object')
# --------------------- Login GET JSON Check --------------------- #
# send a GET request to retrieve the same claims as above
try:
response = self.sessions[i].get('http://%s:%s/api/login' % (self.hostname, self.port),
timeout=2)
except requests.exceptions.RequestException:
raise AssertionError("The server did not respond within 2s")
try:
# Convert the response to JSON
data = response.json()
# ensure all expected fields are present
assert 'iat' in data, "Could not find the claim 'iat' in the JSON object."
assert 'exp' in data, "Could not find the claim 'exp' in the JSON object."
assert 'sub' in data, "Could not find the claim 'sub' in the JSON object."
# Verify that the two timestamps are valid dates from self.current_year
assert datetime.fromtimestamp(data['iat']).year == self.current_year, "'iat' returned is not a valid date"
assert datetime.fromtimestamp(data['exp']).year == self.current_year, "'exp' returned is not a valid date"
# Verify that the subject claim to is set to the right username
assert data['sub'] == self.username, "The subject claim 'sub' should be set to %s" % self.username
except ValueError:
raise AssertionError('The login GET API did not return a valid JSON object')
# Sleep for a short duration before testing again # Sleep for a short duration before testing again
time.sleep(random.random() / 10.0) time.sleep(random.random() / 10.0)
@ -2005,6 +1966,7 @@ class Authentication(Doc_Print_Test_Case):
response = self.sessions[i].post('http://%s:%s/api/login' % (self.hostname, self.port), response = self.sessions[i].post('http://%s:%s/api/login' % (self.hostname, self.port),
json={'username': self.username, 'password': self.password}, json={'username': self.username, 'password': self.password},
timeout=2) timeout=2)
except requests.exceptions.RequestException: except requests.exceptions.RequestException:
raise AssertionError("The server did not respond within 2s") raise AssertionError("The server did not respond within 2s")
@ -2016,6 +1978,12 @@ class Authentication(Doc_Print_Test_Case):
for cookie in self.sessions[i].cookies: for cookie in self.sessions[i].cookies:
try: try:
self.assertEquals(cookie.path, "/", "Cookie path should be /")
self.assertTrue("HttpOnly" in cookie._rest, "Cookie is not http only.")
maxage = cookie.expires - time.mktime(datetime.now().timetuple())
if abs(maxage - int(auth_token_expiry)) > 1:
raise AssertionError(f"Cookie's Max-Age is {maxage} should be {auth_token_expiry}")
encoded_data = cookie.value.split('.')[1] encoded_data = cookie.value.split('.')[1]
# Try to decode the payload # Try to decode the payload
@ -2233,7 +2201,7 @@ class VideoStreaming(Doc_Print_Test_Case):
""" """
# build a collection of URLs to try # build a collection of URLs to try
url_prefix = "http://%s:%s" % (self.hostname, self.port) url_prefix = "http://%s:%s" % (self.hostname, self.port)
resources = ["/", "/index.html", "/public/index.html", "/api/login", "/api/video", "/v1.mp4"] resources = ["/index.html", "/public/index.html", "/v1.mp4"]
# do the following for each URL # do the following for each URL
occurrences = 0 occurrences = 0
@ -2252,7 +2220,7 @@ class VideoStreaming(Doc_Print_Test_Case):
# make sure the correct status code was received # make sure the correct status code was received
if response.status_code != requests.codes.ok: if response.status_code != requests.codes.ok:
raise AssertionError("Server responded with %d instead of 200 OK when requested with %s" % raise AssertionError("Server responded with %d instead of 200 OK when requested with %s" %
response.status_code, resource) (response.status_code, resource))
# search the header dictionary (lowercase comparison) for Accept-Ranges # search the header dictionary (lowercase comparison) for Accept-Ranges
accept_ranges_expect = "bytes" accept_ranges_expect = "bytes"
@ -2262,11 +2230,12 @@ class VideoStreaming(Doc_Print_Test_Case):
# make sure the correct value is given ("bytes") # make sure the correct value is given ("bytes")
if "bytes" not in accept_ranges: if "bytes" not in accept_ranges:
raise AssertionError("Server responded with an unexpected Accept-Ranges values. " raise AssertionError("Server responded with an unexpected Accept-Ranges values. "
"Expected: %s, received: %s" % (accept_ranges_expect, response.headers[header])) "Expected: %s, received: %s" % (accept_ranges_expect, response.headers["Accept-Ranges"]))
# if no occurrences were found, throw an error # if no occurrences were found, throw an error
if occurrences == 0: if occurrences == 0:
raise AssertionError("Failed to find the Accept-Ranges header in the server's responses.") raise AssertionError("Failed to find the Accept-Ranges header in the server's responses. "
"Your server must send 'Accept-Ranges: bytes' in its HTTP responses when serving static files.")
def test_video_get(self): def test_video_get(self):
""" Test Name: test_video_get """ Test Name: test_video_get
@ -2329,7 +2298,7 @@ class VideoStreaming(Doc_Print_Test_Case):
vidsize = os.path.getsize(self.vids[0]) vidsize = os.path.getsize(self.vids[0])
url = "http://%s:%s/%s" % (self.hostname, self.port, vid) url = "http://%s:%s/%s" % (self.hostname, self.port, vid)
# set up a few range request values to test with the video # set up a few range request values to test with the video
ranges = [[0, 1], [0, 100], [300, 500], [1000, -1], [-1, 1000]] ranges = [[0, 1], [0, 100], [300, 500], [1000, -1]]#, [-1, 1000]]
# iterate across each range array to test each one # iterate across each range array to test each one
for rg in ranges: for rg in ranges:
@ -2347,21 +2316,22 @@ class VideoStreaming(Doc_Print_Test_Case):
prepared_req.url = url prepared_req.url = url
response = self.session.send(prepared_req, timeout=2) response = self.session.send(prepared_req, timeout=2)
except requests.exceptions.RequestException: except requests.exceptions.RequestException:
raise AssertionError("The server did not respond within 2s") raise AssertionError("The server did not respond within 2s\nRange request sent: '%s'" % rgheader)
# make sure the correct status code was received # make sure the correct status code was received
if response.status_code != requests.codes.partial_content: if response.status_code != requests.codes.partial_content:
raise AssertionError("Server responded with %d instead of 206 PARTIAL CONTENT when range-requested with a valid video" % raise AssertionError("Server responded with %d instead of 206 PARTIAL CONTENT when range-requested with a valid video"
response.status_code) "\nRange request sent: '%s'" % (response.status_code, rgheader))
# check for the content-type header # check for the content-type header
content_type = self.find_header(response, "Content-Type") content_type = self.find_header(response, "Content-Type")
content_expect = "video/mp4" content_expect = "video/mp4"
if content_type == None: if content_type == None:
raise AssertionError("Server didn't respond with the Content-Type header when requested with a valid video") raise AssertionError("Server didn't respond with the Content-Type header when requested with a valid video"
"\nRange request sent: '%s'" % rgheader)
if content_type.lower() != content_expect: if content_type.lower() != content_expect:
raise AssertionError("Server didn't respond with the correct Content-Type value when requested with a valid video. " raise AssertionError("Server didn't respond with the correct Content-Type value when requested with a valid video. "
"Expected: %s, received: %s" % (content_expect, content_type)) "Expected: %s, received: %s\nRange request sent: '%s'" % (content_expect, content_type, rgheader))
# check for the content-length header and make sure it's the correct # check for the content-length header and make sure it's the correct
# value based on the current range value we're trying # value based on the current range value we're trying
@ -2372,27 +2342,29 @@ class VideoStreaming(Doc_Print_Test_Case):
elif rg[1] == -1: elif rg[1] == -1:
content_length_expect = vidsize - rg[0] content_length_expect = vidsize - rg[0]
if content_length == None: if content_length == None:
raise AssertionError("Server didn't respond with the Content-Length header when requested with a valid video") raise AssertionError("Server didn't respond with the Content-Length header when requested with a valid video"
"\nRange request sent: '%s'" % rgheader)
if content_length != str(content_length_expect): if content_length != str(content_length_expect):
raise AssertionError("Server didn't respond with the correct Content-Length value when requested with a valid video. " raise AssertionError("Server didn't respond with the correct Content-Length value when requested with a valid video. "
"Expected: %s, received: %s" % (content_length_expect, content_length)) "Expected: %s, received: %s\nRange request sent: '%s'" % (content_length_expect, content_length, rgheader))
# check for the Content-Range header and make sure it's the correct # check for the Content-Range header and make sure it's the correct
# value # value
content_range = self.find_header(response, "Content-Range") content_range = self.find_header(response, "Content-Range")
byte_start = rg[0] if rg[0] != -1 else vidsize - rg[1] byte_start = rg[0] if rg[0] != -1 else vidsize - rg[1]
content_range_expect = "bytes %d-%d/%d" % (byte_start, byte_start + content_length_expect, vidsize) content_range_expect = "bytes %d-%d/%d" % (byte_start, byte_start + content_length_expect - 1, vidsize)
if content_range == None: if content_range == None:
raise AssertionError("Server didn't respond with the Content-Range header when requested with a valid video") raise AssertionError("Server didn't respond with the Content-Range header when requested with a valid video"
if content_type.lower() != content_expect: "\nRange request sent: '%s'" % rgheader)
if content_range.lower() != content_range_expect:
raise AssertionError("Server didn't respond with the correct Content-Range value when requested with a valid video. " raise AssertionError("Server didn't respond with the correct Content-Range value when requested with a valid video. "
"Expected: '%s', received: '%s'" % (content_range_expect, content_range)) "Expected: '%s', received: '%s'\nRange request sent: '%s'" % (content_range_expect, content_range, rgheader))
# finally, we'll compare the actual bytes that were received. They # finally, we'll compare the actual bytes that were received. They
# must match the exact bytes found in the original file # must match the exact bytes found in the original file
if not self.compare_file_bytes(self.vids[0], response, byte_start, content_length_expect): if not self.compare_file_bytes(self.vids[0], response, byte_start, content_length_expect):
raise AssertionError("Server didn't send the correct bytes. Should have been bytes %d-%d" % raise AssertionError("Server didn't send the correct bytes. Should have been bytes %d-%d"
(byte_start, byte_start + content_length_expect - 1)) "\nRange request sent: '%s'" % (byte_start, byte_start + content_length_expect - 1, rgheader))
############################################################################### ###############################################################################
@ -2762,6 +2734,12 @@ process.
if individual_test is not None: if individual_test is not None:
single_test_suite = unittest.TestSuite() single_test_suite = unittest.TestSuite()
testclass = findtest(individual_test) testclass = findtest(individual_test)
# make sure the class was found
if testclass == None:
print("Couldn't find a test with the name '%s'" % individual_test)
sys.exit(1)
if testclass == Authentication: if testclass == Authentication:
killserver(server) killserver(server)
server.wait() server.wait()