[Перевод] Посмертно. Один-единственный пробел

ba4869cadc884598bb59bab7739ee317.pngМы — небольшой стартап в Чарльстоне, Южная Каролина. Мы принимаем заказы с помощью текстовых сообщений и распечатываем их непосредственно в ресторанах.

Мы запустили eatabit.com в Чарльстоне почти год назад. За это время наш API распечатал более 9300 заказов на еду у наших клиентов — это рестораны, стадионы, курсы гольфа. Работа с мобильной связью не отличается простотой — особенно в зонах с повышенной нагрузкой на сеть, как на стадионах во время матчей, но наши системы отслеживают ситуации вроде плохого качества сигнала или разрывов.

2085ecd94da54bce84e4b25bde152e06.png

Ночь со среды на четверг обычно вторая после ночи со вторника на среду по минимальной загрузке. Поэтому, когда мониторы начали выдавать волну предупреждений около полуночи, мы подумали, что это случайность. К сожалению, это была не она…

9eb3b8db8ea54921a235ff632c94f375.png

Что ж, разрывы бывают. Иногда в ресторан приходят новые сотрудники, которые выдёргивают из розетки вилку принтера, чтобы «отключить на ночь», но в этот раз проблема возникла со ВСЕМИ принтерами. Наш поставщик SIM-карт, T-Mobile, иногда перезагружает базовые станции, и на 60–90 секунд отключаются принтеры в целом районе, но не ВСЕ принтеры Чарльстона.

Ок, начинаем решать проблему с лога самих принтеров…

e2fc7e289eca4778862a73a7ea5745e4.png

Принтеры на основе чипсета Arduino Mega (ATMega 1280), поэтому мы запускаем Arduino IDE и изучаем статус системы:

SYSTEM: State: Polling. SYSTEM: Polling: SYSTEM: State: Idle. WEB: Starting Poll! WEB: Poll Request: GET /v1/printer/***************/orders.txt HTTP/1.1 HOST: api.eatabit.io Authorization: Basic *********************************************** User-Agent: Eatabit_Device/0.5.1 Content-Length: 67 Content-Type: application/x-www-form-urlencoded Accept: */* Connection: close pickup_minutes=15&delivery_minutes=30&paper_status=1&cell_signal=15 WEB: Request Length: 351 WEB: Request Page Status: 0 AT+CSQ +CSQ: 20,0 OK GSM: CSQ:20 AT#SKTD=0,80, «api.eatabit.io»,0,0 CONNECT GSM: SEND: Socket Enabled GSM: RCXV: Header Sent HTTP/1.1 505 HTTP Version Not Supported Connection: close Server: Cowboy Date: Wec, 21 Oct 2014 19:22:32 GMT Content-Length: 0 Стоп… что? HTTP/1.1 505 HTTP Version Not Supported Девять месяцев мы работали с этой самой прошивкой Arduino без вопросов, а теперь это? Внезапно? В полночь? По всему городу? Забавно… Что же это такое? Server: Cowboy Наше приложение развернуто на Heroku, мы используем веб-сервер Unicorn. Так что же это за «Ковбой», который общается с нашим сервисом? Так, пробуем cURL и воспроизводим запрос через командную строку на моем Маке: curl «http://api.eatabit.io/v1/printer/***************/orders.text? pickup_minutes=30&delivery_minutes=90&state=ok» \ > -X GET \ > -u ***************:****************** \ > -v \ > --user-agent «Eatabit_Device/0.5.1» * Hostname was NOT found in DNS cache * Trying 54.225.219.28… * Connected to api.eatabit.io (54.225.219.28) port 80 (#0) * Server auth using Basic with user '***************' > GET /v1/printer/***************/orders.text? pickup_minutes=30&delivery_minutes=90&state=ok HTTP/1.1 > Authorization: Basic ***********************************************< > User-Agent: Eatabit_Device/0.5.1 > Host: api.eatabit.io > Accept: */* > < HTTP/1.1 200 OK * Server Cowboy is not blacklisted < Server: Cowboy < Connection: keep-alive < Date: Mon, 21 Oct 2014 01:25:11 GMT < Status: 200 OK < Content-Type: text/plain < Content-Length: 7 < Etag: "26f14889eed7cc4b3b78cc667c7ee9eb" < Cache-Control: max-age=0, private, must-revalidate < X-Request-Id: dce1a529-fff1-4222-b997-21d7c074487f < X-Runtime: 1.135825 < Via: 1.1 vegur < * Connection #0 to host api.eatabit.io left intact {0:0}{}% Вроде всё нормально… WTF?! Смотрим на прошивку Arduino, где создается остальная часть запроса. if(isCommsBufferAvailable()) { //make the chunk uint8_t dataLength = snprintf(( char * )tempString, sizeof(tempString), "pickup_minutes=%hhu&delivery_minutes=%hhu&paper_status=%hhu&cell_signal=%hhu", settings.getPickupMinutes(), settings.getDeliveryMinutes(), printer.hasPaper(), gsmModem.getSignal() ); uint8_t contentLength[4]; dataLength = strlen( ( char * ) tempString); itoa(dataLength,( char * ) contentLength, 10); IF_DEBUG(DEBUG_INFO) PORT_DEBUG.println("WEB::Starting Poll!"); reserveCommsBuffer(); //build the header for the POLL request strcpy( ( char * ) commsOrderBuffer, "GET /v1/printer/"); strcat( ( char * ) commsOrderBuffer, ( char * ) settings.getIMEI()); strcat( ( char * ) commsOrderBuffer, "/orders.txt HTTP/1.1\r\n"); strcat( ( char * ) commsOrderBuffer, "HOST: "); strcat( ( char * ) commsOrderBuffer, SERVER_NAME); strcat( ( char * ) commsOrderBuffer, "\r\n"); strcat( ( char * ) commsOrderBuffer, "Authorization: Basic "); strcat( ( char * ) commsOrderBuffer, ( char * ) settings.getAccess()); strcat( ( char * ) commsOrderBuffer, "\r\n"); strcat( ( char * ) commsOrderBuffer, "User-Agent: Eatabit_Device/"); strcat( ( char * ) commsOrderBuffer, SOFTWARE_VER); strcat( ( char * ) commsOrderBuffer, "\r\n"); strcat( ( char * ) commsOrderBuffer, "Content-Length: "); strcat( ( char * ) commsOrderBuffer, ( char * ) contentLength); strcat( ( char * ) commsOrderBuffer, "\r\n"); strcat( ( char * ) commsOrderBuffer, "Content-Type: application/x-www-form-urlencoded\r\n"); strcat( ( char * ) commsOrderBuffer, "Accept: */*\r\n"); strcat( ( char * ) commsOrderBuffer, "Connection: close\r\n"); strcat( ( char * ) commsOrderBuffer, "\r\n"); strcat( ( char * ) commsOrderBuffer, ( char * ) tempString); IF_DEBUG(DEBUG_BUFFERS) { temp1 = strlen((char*) commsOrderBuffer); PORT_DEBUG.println("WEB::Poll Request: "); PORT_DEBUG.println((char*)commsOrderBuffer); PORT_DEBUG.print("WEB::Request Length: "); PORT_DEBUG.println(temp1); } temp1 = gsmModem.requestPage( (uint8_t *) SERVER_NAME, PORT_NUMBER, commsOrderBuffer, commsOrderBuffer, ORDER_QUE_MAX_ORDER_SIZE); IF_DEBUG(DEBUG_BUFFERS) { PORT_DEBUG.print("WEB::Request Page Status: "); PORT_DEBUG.println(temp1); } //go wait for the response _state = WEB_WAIT_POLL; } else { IF_DEBUG(DEBUG_BUFFERS) PORT_DEBUG.println("WEB::Buffer In use!"); } break; Стойте. Неужто это… между URL и HTTP версиями строки есть разница — дополнительный пробел: strcat( ( char * ) commsOrderBuffer, "/orders.txt HTTP/1.1\r\n"); Давайте проверим теорию, что Ковбой не может обработать более одного символа пробела между анализируемыми параметрами, добавив дополнительный пробел к концу URL. curl "http://api.eatabit.io/v1/printer/***************/orders.text?pickup_minutes=30&delivery_minutes=90&state=ok " \ -X GET \ -u ***************:****************** \ -v \ --user-agent "Eatabit_Device/0.5.1" * Hostname was NOT found in DNS cache * Trying 54.225.123.174... * Connected to api.eatabit.io (54.225.123.174) port 80 (#0) * Server auth using Basic with user '***************' > GET /v1/printer/***************/orders.text? pickup_minutes=30&delivery_minutes=90&state=ok HTTP/1.1 > Authorization: Basic ***********************************************<< > User-Agent: Eatabit_Device/0.5.1 > Host: api.eatabit.io > Accept: */* > < HTTP/1.1 505 HTTP Version Not Supported < Connection: close * Server Cowboy is not blacklisted < Server: Cowboy < Date: Mon, 27 Oct 2014 02:03:25 GMT < Content-Length: 0 < * Closing connection 0 Есть!cde50f0b7b4a4ccda28cc38e24935f48.png

Таким образом, в нашем коде обнаружен баг (в форме лишнего пробела). Но почему же это все внезапно началось? Ведь этот баг существовал как минимум девять месяцев на работающей прошивке…

f1b129b225ae45a4a93c153ace9d0ea4.png

Что же изменилось?

Server: Cowboy Cowboy. Кто запустил этот сервер, который по-тихому заблокировал (точнее, сделал некорректными) наши запросы? Heroku? AWS? Поверхностный поиск в Гугле не дал определенного ответа на этот вопрос. Мы отправили запросы и туда, и туда, и все еще ждем ответа. Оставайтесь на связи.

© Habrahabr.ru