Skip to content

Commit 82622af

Browse files
authored
Ethernet: More HTTP response codes (#337)
* Update version. * Add minimal handling of OPTIONS request. * Handle empty connection. * 404 HTTP response * 408 HTTP response (timeout) * 408 timeout test * 500 HTTP response (internal error) * 405, 500; consolidate functions * COUNTING_FILES state * rootdirSetup() * No HEAD requests (yet?) * Reorder definitions * size correction * Remove sdError() dependency; reclaim 6 bytes * Test for 404 File Not Found * Test for 405 Not Allowed (via OPTIONS request) * Test for 501 Not Implemented * Allow SD.exists() in tests, as before
1 parent f5dc1aa commit 82622af

10 files changed

Lines changed: 281 additions & 71 deletions

library.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name=TankController
2-
version=22.8.1
2+
version=22.8.2
33
author=Kirt Onthank <Kirt.Onthank@wallawalla.edu>, Preston Carman <prestonc@apache.org>, James Foster <github@jgfoster.net>
44
maintainer=James Foster <github@jgfoster.net>
55
sentence=Software for the Arduino that controls pH and temperature in the Open-Acidification project.

src/Devices/EthernetServer_TC.cpp

Lines changed: 108 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010
#include "TankController.h"
1111

1212
// class variables
13-
EthernetServer_TC* EthernetServer_TC::_instance = nullptr;
13+
EthernetServer_TC *EthernetServer_TC::_instance = nullptr;
1414

1515
// class methods
1616
/**
1717
* accessor for singleton
1818
*/
19-
EthernetServer_TC* EthernetServer_TC::instance() {
19+
EthernetServer_TC *EthernetServer_TC::instance() {
2020
if (!_instance) {
2121
_instance = new EthernetServer_TC(80);
2222
}
@@ -69,17 +69,25 @@ void EthernetServer_TC::get() {
6969
fileSetup();
7070
} else {
7171
serial(F("get \"%s\" not recognized!"), buffer + 4);
72-
sendBadRequestHeaders();
72+
sendResponse(HTTP_NOT_FOUND);
7373
state = FINISHED;
7474
}
7575
}
7676

77+
// Handles an HTTP OPTIONS request
78+
void EthernetServer_TC::options() {
79+
// Method not allowed
80+
sendResponse(HTTP_NOT_PERMITTED);
81+
state = FINISHED;
82+
}
83+
7784
// Handles an HTTP POST request
7885
void EthernetServer_TC::post() {
7986
if (memcmp_P(buffer + 6, F("api"), 3) == 0) {
8087
keypress();
8188
} else {
8289
serial(F("post \"%s\" not recognized!"), buffer + 6);
90+
sendResponse(HTTP_BAD_REQUEST);
8391
state = FINISHED;
8492
}
8593
}
@@ -99,21 +107,22 @@ void EthernetServer_TC::apiHandler() {
99107
} else if (memcmp_P(buffer + 11, F("display"), 7) == 0) {
100108
display();
101109
} else if (memcmp_P(buffer + 11, F("rootdir"), 7) == 0) {
102-
rootdir();
110+
rootdirSetup();
103111
} else if (memcmp_P(buffer + 11, F("testRead"), 8) == 0) {
104112
testReadSpeed();
105113
} else if (memcmp_P(buffer + 11, F("testWrite"), 9) == 0) {
106114
testWriteSpeed();
107115
} else {
108116
// Unimplemented in API 1
109117
serial(F("Request unimplemented in API 1"));
110-
sendBadRequestHeaders();
118+
sendResponse(HTTP_BAD_REQUEST);
111119
state = FINISHED;
112120
}
113121
} else {
114122
// Later API versions may be implemented here
115123
serial(F("unhandled API version"));
116-
sendBadRequestHeaders();
124+
sendResponse(HTTP_BAD_REQUEST);
125+
;
117126
state = FINISHED;
118127
}
119128
}
@@ -122,7 +131,7 @@ void EthernetServer_TC::apiHandler() {
122131
void EthernetServer_TC::current() {
123132
JSONBuilder builder;
124133
int size = builder.buildCurrentValues();
125-
char* text = builder.bufferPtr();
134+
char *text = builder.bufferPtr();
126135
// First send headers
127136
sendHeadersWithSize(size);
128137
// Write JSON file to client (will be null-terminated)
@@ -150,24 +159,24 @@ void EthernetServer_TC::display() {
150159
void EthernetServer_TC::keypress() {
151160
if (buffer[23] != ' ') {
152161
serial(F("value too long"));
153-
sendBadRequestHeaders();
162+
sendResponse(HTTP_BAD_REQUEST);
154163
} else {
155164
// We have a one character keypress, check to see if valid character
156165
char key = buffer[22];
157166
if (key == '#' || key == '*' || (key >= '0' && key <= '9') || (key >= 'A' && key <= 'D')) {
158167
// States will handle keypresses appropriately
159168
TankController::instance()->setNextKey(key);
160-
sendRedirectHeaders();
169+
sendResponse(HTTP_REDIRECT);
161170
} else {
162171
serial(F("bad character: %c"), key);
163-
sendBadRequestHeaders();
172+
sendResponse(HTTP_BAD_REQUEST);
164173
}
165174
}
166175
state = FINISHED;
167176
}
168177

169178
// Non-member callback wrapper for singleton
170-
void writeToClientBufferCallback(char* buffer, bool isFinished) {
179+
void writeToClientBufferCallback(char *buffer, bool isFinished) {
171180
// The boolean value in the callback is true when the process is complete
172181
EthernetServer_TC::instance()->writeToClientBuffer(buffer, isFinished);
173182
}
@@ -178,30 +187,37 @@ void countFilesCallback(int fileCount) {
178187
EthernetServer_TC::instance()->sendHeadersForRootdir(fileCount);
179188
}
180189

181-
// List the root directory to the client
182-
void EthernetServer_TC::rootdir() {
183-
// Call function on SD Card
184-
// Provide callback to call when writing to the client buffer
185-
if (state != LISTING_FILES) {
186-
state = LISTING_FILES;
187-
isDoneCountingFiles = false;
188-
startTime = millis();
189-
serial(F("Preparing list of files in root directory..."));
190-
} else {
191-
if (isDoneCountingFiles) {
192-
SD_TC::instance()->listRootToBuffer(writeToClientBufferCallback);
193-
} else {
190+
// Count files in root directory so that the Content-Length
191+
// for the header may be calculated
192+
void EthernetServer_TC::rootdirSetup() {
193+
if (state == COUNTING_FILES) {
194194
#ifndef MOCK_PINS_COUNT
195-
SD_TC::instance()->countFiles(countFilesCallback);
195+
if (!SD_TC::instance()->countFiles(countFilesCallback)) {
196+
sendResponse(HTTP_ERROR);
197+
state = FINISHED;
198+
};
196199
#else
197-
countFilesCallback(0);
200+
countFilesCallback(0);
198201
#endif
199-
}
202+
} else {
203+
state = COUNTING_FILES;
204+
startTime = millis();
205+
serial(F("Preparing list of files in root directory..."));
200206
}
201207
}
202208

209+
// List the root directory to the client
210+
void EthernetServer_TC::rootdir() {
211+
// Call function on SD Card
212+
// Provide callback to call when writing to the client buffer
213+
if (!SD_TC::instance()->listRootToBuffer(writeToClientBufferCallback)) {
214+
sendResponse(HTTP_ERROR);
215+
state = FINISHED;
216+
};
217+
}
218+
203219
// Write to the client buffer
204-
void EthernetServer_TC::writeToClientBuffer(char* buffer, bool isFinished) {
220+
void EthernetServer_TC::writeToClientBuffer(char *buffer, bool isFinished) {
205221
// Write to client and return (ASSUME NULL-TERMINATED)
206222
client.write(buffer);
207223
if (isFinished) {
@@ -215,12 +231,10 @@ void EthernetServer_TC::writeToClientBuffer(char* buffer, bool isFinished) {
215231

216232
void EthernetServer_TC::sendHeadersForRootdir(int fileCount) {
217233
#ifndef MOCK_PINS_COUNT
218-
isDoneCountingFiles = true;
219234
serial(F("...%i files..."), fileCount);
220235
sendHeadersWithSize((uint32_t)fileCount * 24); // 24 characters per line
221-
state = LISTING_FILES; // TODO: This is here only because sendHeadersWithSize() changes the state prematurely.
236+
state = LISTING_FILES;
222237
#else
223-
isDoneCountingFiles = true;
224238
sendHeadersWithSize((uint32_t)49);
225239
state = LISTING_FILES;
226240
#endif
@@ -331,8 +345,10 @@ void EthernetServer_TC::loop() {
331345
state = FINISHED;
332346
}
333347
break;
348+
case COUNTING_FILES:
349+
rootdirSetup();
350+
break;
334351
case LISTING_FILES:
335-
// Listing files from SD Card
336352
rootdir();
337353
break;
338354
case NOT_CONNECTED:
@@ -348,24 +364,33 @@ void EthernetServer_TC::loop() {
348364
break;
349365
}
350366
}
351-
if (bufferContentsSize > 0) {
367+
if (bufferContentsSize == 0) {
368+
if (millis() - connectedAt > TIMEOUT) {
369+
sendResponse(HTTP_TIMEOUT);
370+
state = FINISHED;
371+
}
372+
} else {
352373
if (memcmp_P(buffer, F("GET "), 4) == 0) {
353374
state = GET_REQUEST;
354375
get();
355-
break;
356376
} else if (memcmp_P(buffer, F("POST "), 5) == 0) {
357377
state = POST_REQUEST;
358378
post();
359-
break;
379+
} else if (memcmp_P(buffer, F("OPTIONS "), 8) == 0) {
380+
state = OPTIONS_REQUEST;
381+
options();
360382
} else {
361383
serial(buffer);
362-
serial(F("Bad or unsupported request"));
363-
sendBadRequestHeaders();
384+
serial(F("Unsupported request"));
385+
sendResponse(HTTP_NOT_IMPLEMENTED);
364386
state = FINISHED;
365-
break;
366387
}
367388
}
389+
break;
368390
default:
391+
serial(F("loop() - unknown state: %i\nReseting state"), (int)state);
392+
sendResponse(HTTP_ERROR);
393+
state = FINISHED;
369394
break;
370395
}
371396
} else if (state != NOT_CONNECTED) { // In case client disconnects early
@@ -404,29 +429,58 @@ void EthernetServer_TC::sendHeadersWithSize(uint32_t size) {
404429
// blank line indicates end of headers
405430
client.write('\r');
406431
client.write('\n');
407-
state = FINISHED; // TODO: Why?! This is awkward when we want to send a body next.
408432
}
409433

410-
// 303 response
411-
void EthernetServer_TC::sendRedirectHeaders() {
412-
static const char response[] PROGMEM =
434+
void EthernetServer_TC::sendResponse(int code) {
435+
static const char response_303[] PROGMEM =
413436
"HTTP/1.1 303 See Other\r\n"
414437
"Location: /api/1/display\r\n"
415438
"Access-Control-Allow-Origin: *\r\n"
416439
"\r\n";
417-
char buffer[sizeof(response)];
418-
strncpy_P(buffer, (PGM_P)response, sizeof(buffer));
419-
client.write(buffer);
420-
state = FINISHED;
421-
}
422-
423-
// 400 response
424-
void EthernetServer_TC::sendBadRequestHeaders() {
425-
char buffer[30];
426-
static const char response[] PROGMEM = "HTTP/1.1 400 Bad Request\r\n\r\n";
427-
strncpy_P(buffer, (PGM_P)response, sizeof(buffer));
440+
static const char response_400[] PROGMEM =
441+
"HTTP/1.1 400 Bad Request\r\n"
442+
"\r\n";
443+
static const char response_404[] PROGMEM =
444+
"HTTP/1.1 404 Not Found\r\n"
445+
"\r\n";
446+
static const char response_405[] PROGMEM =
447+
"HTTP/1.1 405 Method Not Allowed\r\n"
448+
"Allow: GET, POST\r\n"
449+
"\r\n";
450+
static const char response_408[] PROGMEM =
451+
"HTTP/1.1 408 Request Timeout\r\n"
452+
"Connection: close\r\n"
453+
"\r\n";
454+
static const char response_500[] PROGMEM =
455+
"HTTP/1.1 500 Internal Server Error\r\n"
456+
"\r\n";
457+
static const char response_501[] PROGMEM =
458+
"HTTP/1.1 501 Not Implemented\r\n"
459+
"\r\n";
460+
char buffer[sizeof(response_303)];
461+
switch (code) {
462+
case HTTP_REDIRECT:
463+
strncpy_P(buffer, (PGM_P)response_303, sizeof(buffer));
464+
break;
465+
case HTTP_BAD_REQUEST:
466+
strncpy_P(buffer, (PGM_P)response_400, sizeof(buffer));
467+
break;
468+
case HTTP_NOT_FOUND:
469+
strncpy_P(buffer, (PGM_P)response_404, sizeof(buffer));
470+
break;
471+
case HTTP_NOT_PERMITTED:
472+
strncpy_P(buffer, (PGM_P)response_405, sizeof(buffer));
473+
break;
474+
case HTTP_TIMEOUT:
475+
strncpy_P(buffer, (PGM_P)response_408, sizeof(buffer));
476+
break;
477+
case HTTP_NOT_IMPLEMENTED:
478+
strncpy_P(buffer, (PGM_P)response_501, sizeof(buffer));
479+
break;
480+
default:
481+
strncpy_P(buffer, (PGM_P)response_500, sizeof(buffer));
482+
};
428483
client.write(buffer);
429-
state = FINISHED;
430484
}
431485

432486
// Calculate day of week in proleptic Gregorian calendar. Sunday == 0.

src/Devices/EthernetServer_TC.h

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,26 @@
99
#include <Ethernet.h>
1010
#endif
1111

12-
enum serverState_t { NOT_CONNECTED, READ_REQUEST, GET_REQUEST, POST_REQUEST, LISTING_FILES, IN_TRANSFER, FINISHED };
12+
#define TIMEOUT 5000
13+
#define HTTP_REDIRECT 303
14+
#define HTTP_BAD_REQUEST 400
15+
#define HTTP_NOT_FOUND 404
16+
#define HTTP_NOT_PERMITTED 405
17+
#define HTTP_TIMEOUT 408
18+
#define HTTP_ERROR 500
19+
#define HTTP_NOT_IMPLEMENTED 501
20+
21+
enum serverState_t {
22+
NOT_CONNECTED,
23+
READ_REQUEST,
24+
GET_REQUEST,
25+
POST_REQUEST,
26+
OPTIONS_REQUEST,
27+
COUNTING_FILES,
28+
LISTING_FILES,
29+
IN_TRANSFER,
30+
FINISHED
31+
};
1332

1433
/**
1534
* EthernetServer_TC provides wrapper for web server for TankController
@@ -44,23 +63,23 @@ class EthernetServer_TC : public EthernetServer {
4463
unsigned long connectedAt = 0;
4564
File file;
4665
int startTime;
47-
bool isDoneCountingFiles = true; // TODO: Perhaps replace this with a new COUNTING_FILES state
4866

4967
// instance methods: constructor
5068
EthernetServer_TC(uint16_t port);
5169
// instance methods: utility
5270
void sendHeadersWithSize(uint32_t size);
53-
void sendRedirectHeaders();
54-
void sendBadRequestHeaders();
71+
void sendResponse(int);
5572
int weekday(int year, int month, int day);
5673
// instance methods: HTTP
5774
void get();
5875
void post();
76+
void options();
5977
void echo();
6078
void apiHandler();
6179
void current();
6280
void display();
6381
void keypress();
82+
void rootdirSetup();
6483
void rootdir();
6584
void testReadSpeed();
6685
void testWriteSpeed();

0 commit comments

Comments
 (0)