Compare commits
25 Commits
spring2022
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
64ebfbf313 | ||
|
f82dce4835 | ||
|
269ac0c501 | ||
|
f93671ac58 | ||
|
152cd33d83 | ||
|
00151f2e18 | ||
|
aeb235246d | ||
|
a874b62771 | ||
|
4720b6f233 | ||
|
af936cb46e | ||
|
724afe49c1 | ||
|
5516fe8910 | ||
|
45517a03cf | ||
|
6b84d45674 | ||
|
7f4a319353 | ||
|
991fc01c87 | ||
|
bbb018471f | ||
|
ad249906f4 | ||
|
06153db5c1 | ||
|
a5525414c8 | ||
|
721256a343 | ||
|
21327b0f76 | ||
|
c4c35394f4 | ||
|
43dbbe35cf | ||
|
be04f7d339 |
@ -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
21
react-app/.gitignore
vendored
@ -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
36041
react-app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
16
react-app/src/actions/auth.js
vendored
16
react-app/src/actions/auth.js
vendored
@ -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)}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
21
react-app/src/components/Logout.js
vendored
21
react-app/src/components/Logout.js
vendored
@ -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;
|
||||||
|
10
react-app/src/components/TopNavBar.js
vendored
10
react-app/src/components/TopNavBar.js
vendored
@ -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>
|
||||||
|
3
react-app/src/components/forms/LoginForm.js
vendored
3
react-app/src/components/forms/LoginForm.js
vendored
@ -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>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleReset}
|
onClick={handleReset}
|
||||||
|
5
react-app/src/config/index.js
vendored
5
react-app/src/config/index.js
vendored
@ -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
|
||||||
|
}
|
||||||
|
26
react-app/src/containers/AppContainer.js
vendored
26
react-app/src/containers/AppContainer.js
vendored
@ -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);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
4
react-app/src/index.js
vendored
4
react-app/src/index.js
vendored
@ -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
|
||||||
|
11
react-app/src/pages/LoginPage.js
vendored
11
react-app/src/pages/LoginPage.js
vendored
@ -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;
|
||||||
|
10
react-app/src/pages/PlayerPage.js
vendored
10
react-app/src/pages/PlayerPage.js
vendored
@ -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>
|
||||||
|
15
react-app/src/pages/PrivatePage.js
vendored
15
react-app/src/pages/PrivatePage.js
vendored
@ -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 <span>{user.sub}</span>.
|
<p>
|
||||||
|
You have successfully authenticated as user <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>);
|
||||||
}
|
}
|
||||||
|
2
react-app/src/reducers/auth.js
vendored
2
react-app/src/reducers/auth.js
vendored
@ -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 = { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
401
src/http.c
401
src/http.c
@ -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;
|
||||||
}
|
}
|
||||||
|
12
src/http.h
12
src/http.h
@ -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 */
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
44
src/main.c
44
src/main.c
@ -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
1
src/partner.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
["seofelicia", "micahmoore"]
|
51
src/socket.c
51
src/socket.c
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user