Skip to content

Commit 9d9dd2c

Browse files
authored
Merge pull request #20 from cceckman/slongfield/404
Add start-line matching to simple_led_http and 404 response for bad start-line
2 parents 9579bf3 + 7afd93d commit 9d9dd2c

File tree

2 files changed

+142
-21
lines changed

2 files changed

+142
-21
lines changed

http_server/simple_led_http.py

Lines changed: 85 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
from amaranth import Module
1+
from amaranth import Module, Const
22
from amaranth.lib.wiring import In, Out, Component, connect
33

44
from printer import Printer
5+
from stream_mux import StreamMux
6+
from stream_demux import StreamDemux
7+
from string_match import StringMatch
58

69
import session
710

@@ -35,45 +38,108 @@ class SimpleLedHttp(Component):
3538
def elaborate(self, _platform):
3639
m = Module()
3740

38-
response = "\r\n".join(
41+
## Input parsers
42+
parser_demux = m.submodules.parser_demux = StreamDemux(mux_width=2, stream_width=8)
43+
connect(m, self.session.inbound.data, parser_demux.input)
44+
45+
# Match the header for an HTTP/1.0 request to the LED path.
46+
# TODO: If we want to match more than one path, could probably have some common
47+
# matching for the method and protocol. Also, if we want to get out of the
48+
# stone age, this could be HTTP/1.1.
49+
led_header = "POST /led HTTP/1.0\r\n"
50+
led_header_matcher = m.submodules.led_header_matcher = StringMatch(led_header)
51+
HEADER_PARSER_LED = 0
52+
connect(m, led_header_matcher.input, parser_demux.outs[HEADER_PARSER_LED])
53+
54+
# Last parser is just a sink
55+
HEADER_PARSER_SINK = 1
56+
m.d.comb += parser_demux.outs[HEADER_PARSER_SINK].ready.eq(1)
57+
58+
## Responders
59+
response_mux = m.submodules.response_mux = StreamMux(mux_width=2, stream_width=8)
60+
connect(m, response_mux.out, self.session.outbound.data)
61+
62+
ok_response = "\r\n".join(
3963
["HTTP/1.0 200 OK",
4064
"Host: Fomu",
4165
"Content-Type: text/plain; charset=utf-8",
4266
"",
4367
"",
4468
'👍']) + "\r\n"
69+
ok_response = ok_response.encode("utf-8")
70+
ok_printer = m.submodules.ok_printer = Printer(ok_response)
71+
RESPONSE_OK = 0
72+
connect(m, ok_printer.output, response_mux.input[RESPONSE_OK])
4573

46-
response = response.encode("utf-8")
47-
48-
printer = m.submodules.printer = Printer(response)
49-
50-
connect(m, printer.output, self.session.outbound.data)
74+
not_found_response = "\r\n".join(
75+
["HTTP/1.0 404 Not Found",
76+
"Host: Fomu",
77+
"Content-Type: text/plain; charset=utf-8",
78+
"",
79+
"",
80+
'👎']) + "\r\n"
81+
not_found_response = not_found_response.encode("utf-8")
82+
not_found_printer = m.submodules.not_found_printer = Printer(not_found_response)
83+
RESPONSE_404 = 1
84+
connect(m, not_found_printer.output, response_mux.input[RESPONSE_404])
5185

5286
with m.FSM():
87+
with m.State("reset"):
88+
m.d.comb += [
89+
led_header_matcher.reset.eq(1)
90+
]
91+
m.next = "idle"
5392
with m.State("idle"):
93+
m.d.comb += led_header_matcher.reset.eq(0)
94+
m.d.sync += parser_demux.select.eq(HEADER_PARSER_LED)
95+
m.d.sync += response_mux.select.eq(RESPONSE_OK)
5496
m.next = "idle"
5597
with m.If(self.session.inbound.active):
56-
m.next = "parsing"
98+
m.next = "parsing_start"
5799
m.d.sync += self.session.outbound.active.eq(1)
58-
with m.State("parsing"):
100+
with m.State("parsing_start"):
59101
m.d.sync += self.session.outbound.active.eq(1)
60-
m.next = "parsing"
102+
m.next = "parsing_start"
103+
m.d.sync += parser_demux.select.eq(HEADER_PARSER_LED)
104+
# Input finished before header matched, or header failed to match
105+
with m.If(~self.session.inbound.active | led_header_matcher.rejected):
106+
m.next = "writing"
107+
m.d.sync += [
108+
response_mux.select.eq(RESPONSE_404),
109+
parser_demux.select.eq(HEADER_PARSER_SINK),
110+
not_found_printer.en.eq(1),
111+
]
112+
# header matched successfully
113+
with m.If(led_header_matcher.accepted):
114+
m.next = "parsing_remainder"
115+
m.d.sync += parser_demux.select.eq(HEADER_PARSER_SINK)
116+
with m.State("parsing_remainder"): # TODO: #3 - Parse headers + body.
61117
# Consume inbound data (drop it on the floor)
62-
m.d.comb += self.session.inbound.data.ready.eq(1)
118+
m.next = "parsing_remainder"
63119
with m.If(~self.session.inbound.active):
64120
# All the input is done.
65-
# TODO: #3 -- we should make this state transition
66-
# when we have completed reading the request
67-
# OR if the inbound session becomes inactive.
68121
m.next = "writing"
69-
m.d.sync += printer.en.eq(1) # one-shot
122+
m.d.sync += [
123+
response_mux.select.eq(RESPONSE_OK),
124+
ok_printer.en.eq(1),
125+
]
70126
with m.State("writing"):
71-
m.d.sync += printer.en.eq(0)
72-
m.d.sync += self.session.outbound.active.eq(1)
127+
m.d.sync += [
128+
ok_printer.en.eq(0),
129+
not_found_printer.en.eq(0),
130+
self.session.outbound.active.eq(1),
131+
]
73132
m.next = "writing"
74-
with m.If(printer.done):
75-
m.next = "idle"
133+
with m.If( ((response_mux.select == RESPONSE_OK) & ok_printer.done)
134+
| ((response_mux.select == RESPONSE_404) & not_found_printer.done)
135+
):
76136
m.d.sync += self.session.outbound.active.eq(0)
137+
# Can finish writing before all the input is collected,
138+
# since a bad request migh trigger an early 404. Wait
139+
# until the input is done before returning to the reset
140+
# state.
141+
with m.If(~self.session.inbound.active):
142+
m.next = "reset"
77143

78144
# TODO: #3 - Implement for real. Currently has a sync block so simple_led_http_test
79145
# will elaborate.

http_server/simple_led_http_test.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,67 @@ async def driver(ctx):
5858
# Doesn't appear to be a way to _remove_ a testbench;
5959
# I guess .reset() is "just" to allow a different initial state?
6060
with sim.write_vcd(sys.stdout):
61-
sim.run_until(0.1)
61+
sim.run_until(0.001)
6262

6363
# Now that the test is done:
6464
collector.assert_eq(expected_output)
6565

66+
def test_404_handling():
67+
dut = SimpleLedHttp()
68+
sim = Simulator(dut)
69+
sim.add_clock(1e-6)
70+
71+
input = ("POST /bad_uri HTTP/1.0\r\n"
72+
"Host: evil_test\r\n"
73+
"User-Agent: evil-agent\r\n"
74+
"Content-Type: text/bad\r\n"
75+
"\r\n"
76+
"\r\n"
77+
"123456\r\n")
78+
expected_output = ("HTTP/1.0 404 Not Found\r\n"
79+
"Host: Fomu\r\n"
80+
"Content-Type: text/plain; charset=utf-8\r\n"
81+
"\r\n"
82+
"\r\n"
83+
"👎\r\n")
84+
85+
async def driver(ctx):
86+
ctx.set(dut.session.inbound.active, 1)
87+
await ctx.tick().until(dut.session.outbound.active)
88+
89+
in_stream = dut.session.inbound.data
90+
ctx.set(in_stream.valid, 1)
91+
idx = 0
92+
while idx < len(input):
93+
ctx.set(in_stream.payload, ord(input[idx]))
94+
if ctx.get(in_stream.ready):
95+
idx += 1
96+
await ctx.tick()
97+
# After all input data is read, deassert inbound session
98+
ctx.set(dut.session.inbound.active, 0)
99+
# Keep driving clock until the outbound session is deasserted
100+
await ctx.tick().until(~dut.session.outbound.active)
101+
assert not ctx.get(dut.session.outbound.data.valid)
102+
103+
# Add some nice margins for our vcd
104+
await ctx.tick()
105+
106+
sim.add_testbench(driver)
107+
108+
collector = StreamCollector(stream=dut.session.outbound.data)
109+
sim.add_process(collector.collect())
110+
111+
# Doesn't appear to be a way to _remove_ a testbench;
112+
# I guess .reset() is "just" to allow a different initial state?
113+
with sim.write_vcd(sys.stdout):
114+
sim.run_until(0.001)
115+
116+
# Now that the test is done:
117+
collector.assert_eq(expected_output)
118+
119+
120+
66121

67122
if __name__ == "__main__":
68-
# TODO: #3 enable this test
69123
test_ok_handling()
124+
test_404_handling()

0 commit comments

Comments
 (0)