Compare commits
No commits in common. "master" and "spring2022_update" have entirely different histories.
master
...
spring2022
@ -19,7 +19,7 @@ git clone https://github.com/akheron/jansson.git
|
||||
|
||||
git clone https://git@github.com/benmcollins/libjwt.git
|
||||
(cd libjwt;
|
||||
git checkout v1.14.0;
|
||||
git checkout v1.13.1;
|
||||
autoreconf -fi;
|
||||
env PKG_CONFIG_PATH=../deps/lib/pkgconfig:${PKG_CONFIG_PATH} ./configure --prefix=${BASE}/deps;
|
||||
make -j 40 install
|
||||
|
21
react-app/.gitignore
vendored
21
react-app/.gitignore
vendored
@ -1,2 +1,21 @@
|
||||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/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*
|
||||
|
36425
react-app/package-lock.json
generated
36425
react-app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,25 +1,24 @@
|
||||
{
|
||||
"name": "react-app",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.2.2",
|
||||
"bootstrap": "^4.6.1",
|
||||
"formik": "^2.2.9",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-player": "^2.10.0",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router": "^6.4.3",
|
||||
"react-router-dom": "^6.4.3",
|
||||
"reactstrap": "^9.1.5",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-player": "^2.9.0",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-router-dom": "^5.2.1",
|
||||
"reactstrap": "^8.10.1",
|
||||
"redux": "^4.1.2",
|
||||
"redux-thunk": "^2.4.0",
|
||||
"superagent": "^8.0.3",
|
||||
"superagent": "^6.1.0",
|
||||
"toastr": "^2.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"react-scripts": "^5.0.1"
|
||||
"react-scripts": "4.0.3"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
|
@ -1,11 +1,7 @@
|
||||
|
||||
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 2021 to React 17.0.2 and latest version of all other packages.
|
||||
- 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,19 +31,3 @@ 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,23 +1,14 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { logout } from '../actions/auth.js';
|
||||
import { isLoaded } from '../util/loadingObject'
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import store from '../store';
|
||||
|
||||
const Logout = () => {
|
||||
const dispatch = useDispatch();
|
||||
const user = useSelector(state => state.auth);
|
||||
const isAuthenticated = isLoaded(user);
|
||||
|
||||
// log out on render, and navigate to root on success
|
||||
// normally, we would inform the server just in case.
|
||||
document.cookie = "auth_token=";
|
||||
useEffect(() => {
|
||||
dispatch(logout());
|
||||
store.dispatch({ type: "LOGOUT" });
|
||||
}, []);
|
||||
|
||||
if (isAuthenticated)
|
||||
return (<i>Logging you out ....</i>);
|
||||
else
|
||||
return (<Navigate to="/" />);
|
||||
return (<Redirect to="/" />);
|
||||
};
|
||||
|
||||
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>
|
||||
{dropdown.label}
|
||||
</DropdownToggle>
|
||||
<DropdownMenu end>
|
||||
<DropdownMenu right>
|
||||
{dropdown.entries.map((item) =>
|
||||
<DropdownItem key={item.path}>
|
||||
<NavLink to={item.path} key={item.path} tag={RRNavLink}>
|
||||
<NavLink to={item.path} key={item.path} activeClassName="active" tag={RRNavLink}>
|
||||
{item.label}
|
||||
</NavLink>
|
||||
</DropdownItem>
|
||||
@ -70,7 +70,7 @@ const NavBar = (props) => {
|
||||
<Nav className="mr-auto" navbar>
|
||||
{menus.topbar.map((item) =>
|
||||
<NavItem key={item.path}>
|
||||
<NavLink to={item.path} tag={RRNavLink}>
|
||||
<NavLink to={item.path} activeClassName="active" tag={RRNavLink}>
|
||||
{item.label}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
@ -86,11 +86,11 @@ const NavBar = (props) => {
|
||||
|
||||
{isLoaded(user) ?
|
||||
<NavItem>
|
||||
<NavLink tag={RRNavLink} to={props.logoutUrl}>Logout ({user.sub})</NavLink>
|
||||
<NavLink activeClassName="active" tag={RRNavLink} to={props.logoutUrl}>Logout ({user.sub})</NavLink>
|
||||
</NavItem>
|
||||
:
|
||||
<NavItem>
|
||||
<NavLink tag={RRNavLink} to={props.loginUrl}>Login</NavLink>
|
||||
<NavLink activeClassName="active" tag={RRNavLink} to={props.loginUrl}>Login</NavLink>
|
||||
</NavItem>
|
||||
}
|
||||
</Nav>
|
||||
|
3
react-app/src/components/forms/LoginForm.js
vendored
3
react-app/src/components/forms/LoginForm.js
vendored
@ -54,10 +54,9 @@ const LoginForm = (props) => {
|
||||
<ButtonToolbar>
|
||||
<Button
|
||||
type='submit'
|
||||
bsstyle='success'>
|
||||
bsstyle='success' className="mr-2">
|
||||
Submit
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleReset}
|
||||
|
5
react-app/src/config/index.js
vendored
5
react-app/src/config/index.js
vendored
@ -34,7 +34,4 @@ const apiPrefix = `${publicUrl}/api`
|
||||
console.log(`Read configuration. Public_URL: ${publicUrl}`)
|
||||
// console.log(`apiPrefix: ${apiPrefix}`)
|
||||
|
||||
export default { menus,
|
||||
branding: "CS3214 Demo App 2022",
|
||||
apiPrefix, publicUrl
|
||||
}
|
||||
export default { menus, 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 PlayerPage from '../pages/PlayerPage';
|
||||
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { Switch, Route, withRouter } from 'react-router-dom';
|
||||
|
||||
import config from '../config';
|
||||
import config from '../config/';
|
||||
|
||||
/** AppContainer renders the navigation bar on top and its
|
||||
* children in the main part of the page. Its children will
|
||||
@ -19,22 +19,22 @@ import config from '../config';
|
||||
*/
|
||||
const AppContainer = (props) => (
|
||||
<div>
|
||||
<TopNavBar branding={config.branding}
|
||||
<TopNavBar branding="CS3214 Demo App 2021"
|
||||
menus={config.menus}
|
||||
user={props.user}
|
||||
loginUrl={`/login`}
|
||||
logoutUrl={`/logout`}
|
||||
/>
|
||||
<div className="container-fluid marketing">
|
||||
<Routes>
|
||||
<Route exact path={`/`} element={<HomePage />} />
|
||||
<Route path={`/logout`} element={<Logout />} />
|
||||
<Route path={`/login`} element={<LoginPage />} />
|
||||
<Route path={`/public`} element={<PublicPage />} />
|
||||
<Route path={`/protected`} element={<PrivatePage />} />
|
||||
<Route path={`/player`} element={<PlayerPage />} />
|
||||
<Route element={<NotFoundPage />} />
|
||||
</Routes>
|
||||
<Switch>
|
||||
<Route exact path={`/`} component={HomePage} />
|
||||
<Route path={`/logout`} component={Logout} />
|
||||
<Route path={`/login`} component={LoginPage} />
|
||||
<Route path={`/public`} component={PublicPage} />
|
||||
<Route path={`/protected`} component={PrivatePage} />
|
||||
<Route path={`/player`} component={PlayerPage} />
|
||||
<Route component={NotFoundPage} />
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -51,4 +51,4 @@ function mapDispatchToProps(dispatch) {
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AppContainer);
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(AppContainer));
|
||||
|
@ -4,27 +4,33 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Navigate, useLocation } from 'react-router';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Redirect, withRouter } from 'react-router';
|
||||
import { connect } from 'react-redux';
|
||||
import { isLoaded } from '../util/loadingObject';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
user: state.auth
|
||||
};
|
||||
}
|
||||
|
||||
export default function RequireAuthentication(Component) {
|
||||
const wrapper = props => {
|
||||
const location = useLocation();
|
||||
const user = useSelector(state => state.auth);
|
||||
if (isLoaded(user)) {
|
||||
if (isLoaded(props.user)) {
|
||||
return <Component {...props} />;
|
||||
} else {
|
||||
return (
|
||||
<Navigate
|
||||
to={`/login`}
|
||||
state = {{
|
||||
from: location
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: `/login`,
|
||||
state: {
|
||||
from: props.history.location
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return wrapper;
|
||||
return withRouter(connect(mapStateToProps)(wrapper));
|
||||
}
|
||||
|
4
react-app/src/index.js
vendored
4
react-app/src/index.js
vendored
@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
|
||||
@ -23,7 +22,6 @@ toastr.options.positionClass = 'toast-bottom-right';
|
||||
store.dispatch(checklogin());
|
||||
|
||||
const mountPoint = document.getElementById('root');
|
||||
const root = createRoot(mountPoint);
|
||||
const rootNode = (
|
||||
<Provider store={store}>
|
||||
<Router basename={config.publicUrl}>
|
||||
@ -31,6 +29,6 @@ const rootNode = (
|
||||
</Router>
|
||||
</Provider>
|
||||
);
|
||||
root.render(rootNode);
|
||||
ReactDOM.render(rootNode, mountPoint);
|
||||
|
||||
window.jQuery = jQuery; // for toastr
|
||||
|
9
react-app/src/pages/LoginPage.js
vendored
9
react-app/src/pages/LoginPage.js
vendored
@ -5,10 +5,9 @@ import { Card, CardHeader, CardBody, Container, Row, Col } from 'reactstrap';
|
||||
import { login } from '../actions/auth.js';
|
||||
import { isLoading, isLoaded } from '../util/loadingObject'
|
||||
import LoginForm from '../components/forms/LoginForm';
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
const LoginPage = () => {
|
||||
const location = useLocation();
|
||||
const LoginPage = (props) => {
|
||||
const dispatch = useDispatch();
|
||||
function doLogin({username, password}) {
|
||||
dispatch(login(username, password));
|
||||
@ -16,9 +15,9 @@ const LoginPage = () => {
|
||||
|
||||
const user = useSelector(state => state.auth);
|
||||
const isAuthenticated = isLoaded(user);
|
||||
const { from } = location.state || { from: { pathname: "/" } };
|
||||
const { from } = props.location.state || { from: { pathname: "/" } };
|
||||
if (isAuthenticated) {
|
||||
return (<Navigate to={from} />);
|
||||
return (<Redirect to={from} />);
|
||||
}
|
||||
|
||||
return (
|
||||
|
8
react-app/src/pages/PlayerPage.js
vendored
8
react-app/src/pages/PlayerPage.js
vendored
@ -14,8 +14,8 @@ const PlayerPage = () => {
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const videos = useSelector((state) => state.video);
|
||||
const [state, setState] = useState({
|
||||
let videos = useSelector((state) => state.video);
|
||||
let [state, setState] = useState({
|
||||
url: ""
|
||||
});
|
||||
const handleChange = (event) => {
|
||||
@ -61,10 +61,7 @@ const PlayerPage = () => {
|
||||
</ButtonToolbar>
|
||||
</Col>
|
||||
<Col>
|
||||
<FormGroup>
|
||||
<Label for="ddmenu">
|
||||
or select one from the list of
|
||||
</Label>
|
||||
{!isLoaded(videos) ? (
|
||||
<Spinner size="lg" color="primary" />
|
||||
) : (
|
||||
@ -82,7 +79,6 @@ const PlayerPage = () => {
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
)}
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
11
react-app/src/pages/PrivatePage.js
vendored
11
react-app/src/pages/PrivatePage.js
vendored
@ -9,19 +9,16 @@ const PrivatePage = () => {
|
||||
return (<Container>
|
||||
<h1>Welcome to a private page</h1>
|
||||
<Row className="mt-3">
|
||||
<p>
|
||||
You have successfully authenticated as user <tt>{user.sub}</tt>.
|
||||
</p>
|
||||
You have successfully authenticated as user <span>{user.sub}</span>.
|
||||
</Row>
|
||||
<Row className="mt-3">
|
||||
<p>Your token was issued at {new Date(user.iat*1000).toString()},
|
||||
it expires {new Date(user.exp*1000).toString()}</p>
|
||||
Your token was issued at {new Date(user.iat*1000).toString()},
|
||||
it expires {new Date(user.exp*1000).toString()}
|
||||
</Row>
|
||||
<Row className="mt-3">
|
||||
<p> This page is "private" only inasmuch as the front-end does not
|
||||
This page is "private" only inasmuch as the front-end does not
|
||||
display it to unauthenticated users. In a fully-fledged app,
|
||||
this page would now perform API requests that require authentication.
|
||||
</p>
|
||||
</Row>
|
||||
</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))
|
||||
delete newState.loadingStatus;
|
||||
}
|
||||
if (action.type.startsWith('LOGOUT:')) {
|
||||
if (action.type === 'LOGOUT') {
|
||||
newState = { }
|
||||
}
|
||||
|
||||
|
389
src/http.c
389
src/http.c
@ -19,8 +19,6 @@
|
||||
#include <time.h>
|
||||
#include <fcntl.h>
|
||||
#include <linux/limits.h>
|
||||
#include <dirent.h>
|
||||
#include <jansson.h>
|
||||
|
||||
#include "http.h"
|
||||
#include "hexdump.h"
|
||||
@ -28,8 +26,6 @@
|
||||
#include "bufio.h"
|
||||
#include "main.h"
|
||||
|
||||
static const char * NEVER_EMBED_A_SECRET_IN_CODE = "supa secret";
|
||||
|
||||
// Need macros here because of the sizeof
|
||||
#define CRLF "\r\n"
|
||||
#define CR "\r"
|
||||
@ -84,8 +80,6 @@ http_parse_request(struct http_transaction *ta)
|
||||
static bool
|
||||
http_process_headers(struct http_transaction *ta)
|
||||
{
|
||||
ta->from = -1;
|
||||
ta->to = -1;
|
||||
for (;;) {
|
||||
size_t header_offset;
|
||||
ssize_t len = bufio_readline(ta->client->bufio, &header_offset);
|
||||
@ -118,64 +112,9 @@ http_process_headers(struct http_transaction *ta)
|
||||
ta->req_content_len = atoi(field_value);
|
||||
}
|
||||
|
||||
ta->valid_token = false;
|
||||
/* Handle other headers here. Both field_value and field_name
|
||||
* 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;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -211,15 +150,7 @@ add_content_length(buffer_t *res, size_t len)
|
||||
static void
|
||||
start_response(struct http_transaction * ta, buffer_t *res)
|
||||
{
|
||||
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) {
|
||||
case HTTP_OK:
|
||||
@ -343,47 +274,9 @@ guess_mime_type(char *filename)
|
||||
if (!strcasecmp(suffix, ".js"))
|
||||
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";
|
||||
}
|
||||
|
||||
/* 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
|
||||
handle_static_asset(struct http_transaction *ta, char *basedir)
|
||||
@ -395,37 +288,12 @@ handle_static_asset(struct http_transaction *ta, char *basedir)
|
||||
// which? Fix it to avoid indirect object reference (IDOR) attacks.
|
||||
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 (errno == EACCES)
|
||||
return send_error(ta, HTTP_PERMISSION_DENIED, "Permission denied.");
|
||||
else {
|
||||
if (html5_fallback) {
|
||||
memset(fname, 0, PATH_MAX);
|
||||
snprintf(fname, sizeof fname, "%s%s", server_root, "/index.html");
|
||||
} else {
|
||||
else
|
||||
return send_not_found(ta);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Determine file size
|
||||
struct stat st;
|
||||
@ -442,28 +310,9 @@ handle_static_asset(struct http_transaction *ta, char *basedir)
|
||||
http_add_header(&ta->resp_headers, "Content-Type", "%s", guess_mime_type(fname));
|
||||
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;
|
||||
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);
|
||||
if (!success)
|
||||
goto out;
|
||||
@ -477,207 +326,10 @@ out:
|
||||
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
|
||||
handle_api(struct http_transaction *ta)
|
||||
{
|
||||
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;
|
||||
|
||||
return send_error(ta, HTTP_NOT_FOUND, "API not implemented");
|
||||
}
|
||||
|
||||
/* Set up an http client, associating it with a bufio buffer. */
|
||||
@ -687,35 +339,6 @@ http_setup_client(struct http_client *self, struct 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. */
|
||||
bool
|
||||
http_handle_transaction(struct http_client *self)
|
||||
@ -727,10 +350,6 @@ http_handle_transaction(struct http_client *self)
|
||||
if (!http_parse_request(&ta))
|
||||
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))
|
||||
return false;
|
||||
|
||||
@ -754,7 +373,7 @@ http_handle_transaction(struct http_client *self)
|
||||
rc = handle_api(&ta);
|
||||
} else
|
||||
if (STARTS_WITH(req_path, "/private")) {
|
||||
rc = handle_private(&ta);
|
||||
/* not implemented */
|
||||
} else {
|
||||
rc = handle_static_asset(&ta, server_root);
|
||||
}
|
||||
@ -762,5 +381,5 @@ http_handle_transaction(struct http_client *self)
|
||||
buffer_delete(&ta.resp_headers);
|
||||
buffer_delete(&ta.resp_body);
|
||||
|
||||
return is_http_1_1 && rc;
|
||||
return rc;
|
||||
}
|
||||
|
10
src/http.h
10
src/http.h
@ -40,11 +40,6 @@ struct http_transaction {
|
||||
size_t req_body; // ditto
|
||||
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 */
|
||||
enum http_response_status resp_status;
|
||||
@ -52,10 +47,6 @@ struct http_transaction {
|
||||
buffer_t resp_body;
|
||||
|
||||
struct http_client *client;
|
||||
|
||||
off_t from; // range from
|
||||
off_t to; // range to
|
||||
bool range_request;
|
||||
};
|
||||
|
||||
struct http_client {
|
||||
@ -65,6 +56,5 @@ struct http_client {
|
||||
void http_setup_client(struct http_client *, struct bufio *bufio);
|
||||
bool http_handle_transaction(struct http_client *);
|
||||
void http_add_header(buffer_t * resp, char* key, char* fmt, ...);
|
||||
bool validate_token_exp(struct http_transaction *ta, char* token);
|
||||
|
||||
#endif /* _HTTP_H */
|
||||
|
@ -2,10 +2,6 @@
|
||||
* Quick demo of how to use libjwt using a HS256.
|
||||
*
|
||||
* @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 <stdlib.h>
|
||||
@ -13,7 +9,6 @@
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <errno.h>
|
||||
#include <jansson.h>
|
||||
|
||||
static const char * NEVER_EMBED_A_SECRET_IN_CODE = "supa secret";
|
||||
|
||||
@ -61,7 +56,6 @@ main()
|
||||
if (encoded == NULL)
|
||||
die("jwt_encode_str", ENOMEM);
|
||||
|
||||
jwt_free(mytoken);
|
||||
printf("encoded as %s\nTry entering this at jwt.io\n", encoded);
|
||||
|
||||
jwt_t *ymtoken;
|
||||
@ -71,32 +65,9 @@ main()
|
||||
if (rc)
|
||||
die("jwt_decode", rc);
|
||||
|
||||
free(encoded);
|
||||
char *grants = jwt_get_grants_json(ymtoken, NULL); // NULL means all
|
||||
if (grants == NULL)
|
||||
die("jwt_get_grants_json", ENOMEM);
|
||||
|
||||
jwt_free(ymtoken);
|
||||
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);
|
||||
}
|
||||
|
42
src/main.c
42
src/main.c
@ -9,7 +9,6 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <signal.h>
|
||||
#include <pthread.h>
|
||||
#include "buffer.h"
|
||||
#include "hexdump.h"
|
||||
#include "http.h"
|
||||
@ -35,19 +34,6 @@ int token_expiration_time = 24 * 60 * 60;
|
||||
// root from which static files are served
|
||||
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.
|
||||
* For each client, it handles exactly 1 HTTP transaction.
|
||||
@ -55,36 +41,19 @@ static void *thr_func(void *c) {
|
||||
static void
|
||||
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);
|
||||
while (accepting_socket != -1) {
|
||||
fprintf(stderr, "Waiting for client...\n");
|
||||
int client_socket = socket_accept_client(accepting_socket);
|
||||
|
||||
if (client_socket == -1){
|
||||
if (client_socket == -1)
|
||||
return;
|
||||
}
|
||||
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));
|
||||
|
||||
struct http_client client;
|
||||
http_setup_client(&client, bufio_create(client_socket));
|
||||
http_handle_transaction(&client);
|
||||
bufio_close(client.bufio);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < num_clients; i++) {
|
||||
pthread_join(threads[i], NULL);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static void
|
||||
usage(char * av0)
|
||||
@ -93,7 +62,6 @@ usage(char * av0)
|
||||
" -p port port number to bind to\n"
|
||||
" -R rootdir root directory from which to serve files\n"
|
||||
" -e seconds expiration time for tokens in seconds\n"
|
||||
" -a enable HTML5 fallback\n"
|
||||
" -h display this help\n"
|
||||
, av0);
|
||||
exit(EXIT_FAILURE);
|
||||
|
@ -1 +0,0 @@
|
||||
["seofelicia", "micahmoore"]
|
51
src/socket.c
51
src/socket.c
@ -54,57 +54,6 @@ socket_open_bind_listen(char * port_number_string, int backlog)
|
||||
}
|
||||
|
||||
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) {
|
||||
assert (pinfo->ai_protocol == IPPROTO_TCP);
|
||||
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)
|
||||
# change this variable, e.g.
|
||||
#URL=http://hazelnut.rlogin:12345
|
||||
# use URL=http://theta.cs.vt.edu:3000/
|
||||
URL=http://localhost:${PORT}
|
||||
|
||||
# the file in which curl stores cookies across runs
|
||||
@ -34,17 +34,8 @@ curl -v \
|
||||
curl -v \
|
||||
${URL}/private/secret.txt
|
||||
|
||||
# this should succeed since credentials are included (via the cookie jar)
|
||||
# this should succeed since credentials are included
|
||||
curl -v \
|
||||
-b ${COOKIEJAR} \
|
||||
${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,6 +61,27 @@ def get_socket_connection(hostname, port):
|
||||
else:
|
||||
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):
|
||||
"""
|
||||
Run a check of the connection for validity, using a well-formed
|
||||
@ -74,9 +95,7 @@ def run_connection_check_empty_login(http_conn, hostname):
|
||||
server_response = http_conn.getresponse()
|
||||
|
||||
# Check the response status code
|
||||
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."
|
||||
assert server_response.status == OK, "Server failed to respond"
|
||||
|
||||
# Check the data included in the server's response
|
||||
assert check_empty_login_respnse(server_response.read().decode('utf-8')), \
|
||||
@ -88,7 +107,7 @@ def run_404_check(http_conn, obj, hostname):
|
||||
requesting a non-existent URL object.
|
||||
"""
|
||||
|
||||
# GET request for obj
|
||||
# GET request for the object /loadavg
|
||||
http_conn.request("GET", obj, headers={"Host": hostname})
|
||||
|
||||
# Get the server's response
|
||||
@ -100,6 +119,27 @@ def run_404_check(http_conn, obj, hostname):
|
||||
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):
|
||||
"""
|
||||
Check that the unsupported method supplied has either a NOT IMPLEMENTED
|
||||
@ -125,6 +165,92 @@ def print_response(response):
|
||||
for line in lines:
|
||||
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):
|
||||
return response.strip() == "{}"
|
||||
|
||||
@ -259,7 +385,7 @@ class Single_Conn_Protocol_Case(Doc_Print_Test_Case):
|
||||
pass
|
||||
|
||||
sock.send(encode("\r\n"))
|
||||
# If there is a HTTP response, it should be a valid /login
|
||||
# If there is a HTTP response, it should be a valid /loadavg
|
||||
# response.
|
||||
data = ""
|
||||
|
||||
@ -1071,7 +1197,6 @@ class Multi_Conn_Sequential_Case(Doc_Print_Test_Case):
|
||||
Number Connections: 2 \n\
|
||||
Procedure: Run 2 connections simultaneously for simple GET requests:\n\
|
||||
GET /api/login HTTP/1.1
|
||||
NOTE: this test requires HTTP/1.1 persistent connection support.
|
||||
"""
|
||||
|
||||
# Append two connections to the list
|
||||
@ -1095,11 +1220,10 @@ class Multi_Conn_Sequential_Case(Doc_Print_Test_Case):
|
||||
|
||||
|
||||
def test_four_connections(self):
|
||||
""" Test Name: test_four_connections\n\
|
||||
""" Test Name: test_two_connections\n\
|
||||
Number Connections: 4 \n\
|
||||
Procedure: Run 4 connections simultaneously for simple GET requests:\n\
|
||||
GET /api/login HTTP/1.1
|
||||
NOTE: this test requires HTTP/1.1 persistent connection support.
|
||||
"""
|
||||
|
||||
# Append four connections to the list
|
||||
@ -1119,11 +1243,10 @@ class Multi_Conn_Sequential_Case(Doc_Print_Test_Case):
|
||||
run_connection_check_empty_login(http_conn, self.hostname)
|
||||
|
||||
def test_eight_connections(self):
|
||||
""" Test Name: test_eight_connections\n\
|
||||
""" Test Name: test_two_connections\n\
|
||||
Number Connections: 8 \n\
|
||||
Procedure: Run 8 connections simultaneously for simple GET requests:\n\
|
||||
GET /api/login HTTP/1.1
|
||||
NOTE: this test requires HTTP/1.1 persistent connection support.
|
||||
"""
|
||||
|
||||
# Append eight connections to the list
|
||||
@ -1190,13 +1313,13 @@ class Single_Conn_Good_Case(Doc_Print_Test_Case):
|
||||
print("The server has crashed. Please investigate.")
|
||||
|
||||
def test_login_get(self):
|
||||
""" Test Name: test_login_get\n\
|
||||
""" Test Name: test_loadavg_no_callback\n\
|
||||
Number Connections: One \n\
|
||||
Procedure: Simple GET request:\n\
|
||||
GET /api/login HTTP/1.1
|
||||
"""
|
||||
|
||||
# GET request for the object /api/login
|
||||
# GET request for the object /loadavg
|
||||
self.http_connection.request("GET", "/api/login")
|
||||
|
||||
# Get the server's response
|
||||
@ -1252,17 +1375,6 @@ class Access_Control(Doc_Print_Test_Case):
|
||||
# Close the HTTP connection
|
||||
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):
|
||||
""" Test Name: test_access_control_private_valid_token
|
||||
Number Connections: N/A
|
||||
@ -1578,56 +1690,6 @@ class Access_Control(Doc_Print_Test_Case):
|
||||
self.assertEqual(response.status_code, requests.codes.not_found,
|
||||
"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):
|
||||
"""
|
||||
Test cases for HTML 5 fallback, using good requests that expect a
|
||||
@ -1884,7 +1946,6 @@ class Authentication(Doc_Print_Test_Case):
|
||||
self.sessions.append(requests.Session())
|
||||
|
||||
for i in range(30):
|
||||
# ----------------------- Login JSON Check ----------------------- #
|
||||
# Login using the default credentials
|
||||
try:
|
||||
response = self.sessions[i].post('http://%s:%s/api/login' % (self.hostname, self.port),
|
||||
@ -1900,13 +1961,19 @@ class Authentication(Doc_Print_Test_Case):
|
||||
# Convert the response to JSON
|
||||
data = response.json()
|
||||
|
||||
# ensure all expected fields are present
|
||||
# Verify that the JWT contains 'iat'
|
||||
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."
|
||||
|
||||
# Verify that the JWT contains 'sub'
|
||||
assert 'sub' in data, "Could not find the claim 'sub' in the JSON object."
|
||||
|
||||
# verify that the two timestamps are valid dates
|
||||
# Verify that the 'iat' claim to is a valid date from self.current_year
|
||||
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"
|
||||
|
||||
# Verify that the subject claim to is set to the right username
|
||||
@ -1915,34 +1982,6 @@ class Authentication(Doc_Print_Test_Case):
|
||||
except ValueError:
|
||||
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
|
||||
time.sleep(random.random() / 10.0)
|
||||
|
||||
@ -1966,7 +2005,6 @@ class Authentication(Doc_Print_Test_Case):
|
||||
response = self.sessions[i].post('http://%s:%s/api/login' % (self.hostname, self.port),
|
||||
json={'username': self.username, 'password': self.password},
|
||||
timeout=2)
|
||||
|
||||
except requests.exceptions.RequestException:
|
||||
raise AssertionError("The server did not respond within 2s")
|
||||
|
||||
@ -1978,12 +2016,6 @@ class Authentication(Doc_Print_Test_Case):
|
||||
|
||||
for cookie in self.sessions[i].cookies:
|
||||
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]
|
||||
|
||||
# Try to decode the payload
|
||||
@ -2201,7 +2233,7 @@ class VideoStreaming(Doc_Print_Test_Case):
|
||||
"""
|
||||
# build a collection of URLs to try
|
||||
url_prefix = "http://%s:%s" % (self.hostname, self.port)
|
||||
resources = ["/index.html", "/public/index.html", "/v1.mp4"]
|
||||
resources = ["/", "/index.html", "/public/index.html", "/api/login", "/api/video", "/v1.mp4"]
|
||||
|
||||
# do the following for each URL
|
||||
occurrences = 0
|
||||
@ -2220,7 +2252,7 @@ class VideoStreaming(Doc_Print_Test_Case):
|
||||
# make sure the correct status code was received
|
||||
if response.status_code != requests.codes.ok:
|
||||
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
|
||||
accept_ranges_expect = "bytes"
|
||||
@ -2230,12 +2262,11 @@ class VideoStreaming(Doc_Print_Test_Case):
|
||||
# make sure the correct value is given ("bytes")
|
||||
if "bytes" not in accept_ranges:
|
||||
raise AssertionError("Server responded with an unexpected Accept-Ranges values. "
|
||||
"Expected: %s, received: %s" % (accept_ranges_expect, response.headers["Accept-Ranges"]))
|
||||
"Expected: %s, received: %s" % (accept_ranges_expect, response.headers[header]))
|
||||
|
||||
# if no occurrences were found, throw an error
|
||||
if occurrences == 0:
|
||||
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.")
|
||||
raise AssertionError("Failed to find the Accept-Ranges header in the server's responses.")
|
||||
|
||||
def test_video_get(self):
|
||||
""" Test Name: test_video_get
|
||||
@ -2298,7 +2329,7 @@ class VideoStreaming(Doc_Print_Test_Case):
|
||||
vidsize = os.path.getsize(self.vids[0])
|
||||
url = "http://%s:%s/%s" % (self.hostname, self.port, vid)
|
||||
# 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
|
||||
for rg in ranges:
|
||||
@ -2316,22 +2347,21 @@ class VideoStreaming(Doc_Print_Test_Case):
|
||||
prepared_req.url = url
|
||||
response = self.session.send(prepared_req, timeout=2)
|
||||
except requests.exceptions.RequestException:
|
||||
raise AssertionError("The server did not respond within 2s\nRange request sent: '%s'" % rgheader)
|
||||
raise AssertionError("The server did not respond within 2s")
|
||||
|
||||
# make sure the correct status code was received
|
||||
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"
|
||||
"\nRange request sent: '%s'" % (response.status_code, rgheader))
|
||||
raise AssertionError("Server responded with %d instead of 206 PARTIAL CONTENT when range-requested with a valid video" %
|
||||
response.status_code)
|
||||
|
||||
# check for the content-type header
|
||||
content_type = self.find_header(response, "Content-Type")
|
||||
content_expect = "video/mp4"
|
||||
if content_type == None:
|
||||
raise AssertionError("Server didn't respond with the Content-Type header when requested with a valid video"
|
||||
"\nRange request sent: '%s'" % rgheader)
|
||||
raise AssertionError("Server didn't respond with the Content-Type header when requested with a valid video")
|
||||
if content_type.lower() != content_expect:
|
||||
raise AssertionError("Server didn't respond with the correct Content-Type value when requested with a valid video. "
|
||||
"Expected: %s, received: %s\nRange request sent: '%s'" % (content_expect, content_type, rgheader))
|
||||
"Expected: %s, received: %s" % (content_expect, content_type))
|
||||
|
||||
# check for the content-length header and make sure it's the correct
|
||||
# value based on the current range value we're trying
|
||||
@ -2342,29 +2372,27 @@ class VideoStreaming(Doc_Print_Test_Case):
|
||||
elif rg[1] == -1:
|
||||
content_length_expect = vidsize - rg[0]
|
||||
if content_length == None:
|
||||
raise AssertionError("Server didn't respond with the Content-Length header when requested with a valid video"
|
||||
"\nRange request sent: '%s'" % rgheader)
|
||||
raise AssertionError("Server didn't respond with the Content-Length header when requested with a valid video")
|
||||
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. "
|
||||
"Expected: %s, received: %s\nRange request sent: '%s'" % (content_length_expect, content_length, rgheader))
|
||||
"Expected: %s, received: %s" % (content_length_expect, content_length))
|
||||
|
||||
# check for the Content-Range header and make sure it's the correct
|
||||
# value
|
||||
content_range = self.find_header(response, "Content-Range")
|
||||
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 - 1, vidsize)
|
||||
content_range_expect = "bytes %d-%d/%d" % (byte_start, byte_start + content_length_expect, vidsize)
|
||||
if content_range == None:
|
||||
raise AssertionError("Server didn't respond with the Content-Range header when requested with a valid video"
|
||||
"\nRange request sent: '%s'" % rgheader)
|
||||
if content_range.lower() != content_range_expect:
|
||||
raise AssertionError("Server didn't respond with the Content-Range header when requested with a valid video")
|
||||
if content_type.lower() != content_expect:
|
||||
raise AssertionError("Server didn't respond with the correct Content-Range value when requested with a valid video. "
|
||||
"Expected: '%s', received: '%s'\nRange request sent: '%s'" % (content_range_expect, content_range, rgheader))
|
||||
"Expected: '%s', received: '%s'" % (content_range_expect, content_range))
|
||||
|
||||
# finally, we'll compare the actual bytes that were received. They
|
||||
# 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):
|
||||
raise AssertionError("Server didn't send the correct bytes. Should have been bytes %d-%d"
|
||||
"\nRange request sent: '%s'" % (byte_start, byte_start + content_length_expect - 1, rgheader))
|
||||
raise AssertionError("Server didn't send the correct bytes. Should have been bytes %d-%d" %
|
||||
(byte_start, byte_start + content_length_expect - 1))
|
||||
|
||||
|
||||
###############################################################################
|
||||
@ -2734,12 +2762,6 @@ process.
|
||||
if individual_test is not None:
|
||||
single_test_suite = unittest.TestSuite()
|
||||
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:
|
||||
killserver(server)
|
||||
server.wait()
|
||||
|
Loading…
x
Reference in New Issue
Block a user