שלום אורח התחבר

הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

הזינו את כתובת המייל וקבלו את הפוסט היומי בכל בוקר אליכם לתיבה:

הפוסטים של הימים האחרונים בבלוג נעלמו לי הבוקר. חיפוש מהיר במקור הדף חשף שהבעיה היא ב HTML לא תקני שאני כתבתי, משהו בסגנון הזה:

<a href="https://www.google.com'>go to google</a>

רואים את הבעיה? גם אני לא ראיתי. אבל אם מסתכלים טוב רואים שיש מרכאות כפולות בצד אחד של הכתובת וגרש בודד בצד השני.

אבל המרכאות הן בכלל לא הסיפור כאן. הסיפור הגדול יותר הוא על שגיאת הייבוא (שקרתה) כשהאתר ניסה לאכול את הפוסט עם השגיאה, והטיפול הלא נכון במצב הזה: הפוסט נכנס בצורה חלקית ושבר את כל הפוסטים האחרים של אותו שבוע.

המבחן של קוד הוא לא כשהכל עובד אלא כשדברים נשברים. קוד טוב יודע להישבר טוב ולתחום את ההפסד. נשבר? שיישבר, אבל שלא ייקח איתו את כל המערכת.

אנשים אוהבים לדבר על שפות תכנות "לכתיבה בלבד" כלומר כאלה שאי אפשר לקרוא קוד שכתוב בהן. אנחנו שוכחים שקוד קריא הוא מאפיין של המתכנתים הרבה יותר מאשר של השפה. הנה דוגמא קצרה עם אותה התוכנית באותה שפה ובשתי גירסאות שונות. תגידו אתם את מה הייתם מעדיפים לקרוא:

גירסא 1:

from random import randint

x = randint(1, 100)
while True:
    y = int(input('Please guess the value: '))
    if randint(1, 100) > 80:
        messages = ['Too large', 'Too small']
        print(messages[randint(0, 1)])

    else:
        if x > y:
            print('Too small')
        elif x < y:
            print('Too large')
        else:
            print('Bingo! You win')
            break

גירסא 2:

from random import randint

def should_lie(*_, lying_frequency=20):
    return randint(1, 100) < lying_frequency

def random_message():
    messages = ['Too large', 'Too small']
    return messages[randint(0, 1)]

secret_value = randint(1, 100)

while True:
    user_guess = int(input('Please guess the value: '))

    if should_lie(lying_frequency=20):
        print(random_message())
    else:
        if secret_value > user_guess:
            print('Too small')
        elif secret_value < user_guess:
            print('Too large')
        else:
            print('Bingo! You win')
            break

המעבר מגירסא 1 ל-2 דורש מעט מאוד מחשבה. כל מה שצריך הוא להבין את מטרת התוכנית ולתת שמות שמתאימים למטרה. אבל כשמגיעים לגירסא 2 חודש אחרי ברור לגמרי מה המשמעות של כל דבר. אפילו שהתוצאה ארוכה יותר בכמה שורות.

מגבלת קצב גישה (Rate Limit) היא המגבלה הכי חשובה שאתם שוכחים להוסיף ליישומים שלכם. בגרסאות שונות שלה המגבלה תגן עליכם ממתקפות מניעת שירות ובכך תעזור לבנות יישומים חסינים יותר.

בהגדרת המגבלות כדאי לקחת בחשבון את המצבים הבאים:

  1. נרצה לזהות ולחסום בקשות "חשודות", למשל ילדים שמריצים כל מיני סקריפטים אוטומטיים שמחפשים חורים ביישום שלנו.

  2. נרצה לחסום בקשות לגיטימיות שמגיעות בקצב מהיר מידי אם השרת שלנו לא מסוגל לעמוד בקצב (וכך להגן על שאר המשתמשים בשרת). לדוגמא נניח שפיצ'ר החיפוש באתר הוא מאוד מורכב וצורך הרבה משאבים, אז נרצה לוודא שאף פעם השרת לא מחפש יותר מ-X מילים באותו הזמן.

  3. נרצה לחסום פעולות מסוימות לפי משתמשים כדי לאכוף התנהגות לפי כללים שהגדרנו. למשל באתר מידע נרצה לאפשר למשתמשים לקרוא את דפי המידע, אבל אם משתמש קורא יותר מדי דפים בדקה אולי הוא בכלל הריץ סקריפט כדי לרוץ ולסרוק אוטומטית את כל האתר - ואת זה לא נרצה לאפשר.

הכלי שאני עובד איתו עבור הסעיף הראשון נקרא fail2ban. הוא רץ ברמת השרת בלי קשר ליישום ספציפי, קורא את קבצי הלוג ומזהה תבניות חריגות. כשמוצא הוא פשוט חוסם את כתובת ה IP הפוגענית. זה הקישור לדף הבית שלהם:

https://www.fail2ban.org/wiki/index.php/Main_Page

שני הסעיפים האחרים כבר קשורים ליישום ולכן הפיתרון אליהם תלוי בשפת הפיתוח והספריה שבחרתם. נראה שתי דוגמאות.

סביבת הפיתוח Flask המאפשרת פיתוח APIs בפייתון מציעה הרחבה בשם FlaskLimiter. הדוגמא שלהם מספרת את רוב הסיפור:

from flask import Flask
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

app = Flask(__name__)
limiter = Limiter(
    app,
    key_func=get_remote_address,
    default_limits=["200 per day", "50 per hour"]
)
@app.route("/slow")
@limiter.limit("1 per day")
def slow():
    return "24"

@route("/test")
@login_required
@limiter.limit("1 per day", key_func = lambda : current_user.username)
def test_route():
    return "42"

@app.route("/fast")
def fast():
    return "42"

הגדרת מגבלות ברירת המחדל אומרת שהנתיב fast מוגבל ל 200 פניות ביום ובכל מקרה לא יותר מ-50 פניות בשעה. הגדרה ספציפית שמופיעה לגבי הנתיב slow אומרת שניתן לפנות אליו לא יותר מפעם ביום והגדרה נוספת לגבי הנתיב test כבר לוקחת בחשבון את שם המשתמש ומגבילה כל משתמש להפעיל את הנתיב רק פעם אחת ביום.

לסביבת Rails יש פיתרון בשם rack-attack שלוקח גישה קצת שונה. הנה הדוגמא:

# config/initializers/rack_attack.rb (for rails apps)

Rack::Attack.throttle("requests by ip", limit: 5, period: 2) do |request|
  request.ip
end

# Throttle login attempts for a given email parameter to 6 reqs/minute
# Return the email as a discriminator on POST /login requests
Rack::Attack.throttle('limit logins per email', limit: 6, period: 60) do |req|
  if req.path == '/login' && req.post?
    req.params['email']
  end
end

הגדרות rack-attack נכתבות בקובץ איתחול יחיד עבור כל היישום. הבלוק הראשון חוסם גישות לפי כתובת IP כך שלא יתקבלו יותר מ-5 פניות בכל שתי שניות מאותה כתובת. הבלוק השני מגביל ניסיונות התחברות לפי מייל נתון ל 6 בדקה.

היתרון בגישה של Flask Limiter הוא שאנחנו רואים את ההגבלות כל הזמן כחלק מהגדרת הנתיב. יש בזה משהו מאוד נכון כי הגבלת קצב הגישה הרבה פעמים מושפעת ממה הפונקציה עושה. בחירת ערכי defaults טובים (בשתי הגישות) אומרת שאף פעם לא נשכח לשים מגבלת קצב כשיוצרים נתיב חדש.

עובדים בסביבות אחרות ורוצים לשתף איך אתם שמים מגבלת קצב ליישומים שלכם? אשמח לשמוע בתגובות.

אפשר להתקדם לרוחב: לשמוע על כל הדברים המדליקים בעולם התוכן שלכם, על מה שחם היום ועל מה שעדיין בפיתוח.

ואפשר לחפור: לשאול שאלות על הדברים שאנחנו חושבים שיודעים ולפתור את אותן בעיות שוב ושוב כדי להכיר את כל הניואנסינים.

העדפה שלי- את הנושאים המדליקים וההדגמות שיפילו אותי מהכסא אני מעדיף לראות ביוטיוב. כך אני יכול לדלג על החלקים המשעממים. לכיתה נשמור את השאלות והדיונים. קורס מתקדם הוא כזה שעזר לי לראות טוב יותר דברים שחשבתי שאני מבין (וכן זה דורש נכונות ופתיחות של המדריך ושל כל המשתתפים. וזה לגמרי שווה את ההשקעה).

על קו הזינוק, לפני המירוץ, כדאי להזכיר למיכה (ולנו), שכמו שזוגיות לא בונים בתחרות גם לימוד תוכן חדש הוא לא מירוץ. אין סיבה לחפש קיצורי דרך "רק" כדי לפתור את התרגיל או הבעיה שעל הפרק. לימוד הוא כמו ג'וגינג: אנחנו לא רצים בשביל להגיע לסוף אלא בשביל הכושר.

תקראו את התיעוד. במלואו. ואז תמשיכו לחפש מקורות לימוד נוספים. ואז תדעו הכל על הנושא שעל הפרק. ואז לכו לפתור את הבעיה. ואז שוב בדרך אחרת.

וכן, קחו את הזמן. תהנו מהתהליך. אין מבחן בסוף.

את התשובה הקצרה אפשר לקחת מחתול אחד שכמו שאמר לאליס: זה תלוי לאן את רוצה להגיע.

בניגוד לאליס אנחנו יודעים לאן אנחנו רוצים להגיע ולכן גם התשובה הארוכה לא תהיה יותר מדי מסובכת:

הכי טוב ללמוד את הדבר שאתה מתעסק בו ביום-יום. יש ערך עצום בלהיות מומחה בטכנולוגיה מסוימת, מומחיות זה דבר שלוקח המון זמן לפתח ודורש המון פוקוס. לכן היום ההמלצה החד משמעית שלי היא לבחור טכנולוגיה ולדעת עליה הכל.

האתגר כאן הוא לא לכתוב עוד מאותו דבר ובאותו סגנון שאתה כבר כותב ביום-יום, אלא לצלול לעומק הטכנולוגיה וללמוד עליה דברים שבעבודה שוטפת לא היית מגיע ללמוד.

ומה לגבי הרחבת אופקים כללית? לנסות לבנות דברים חדשים בטכנולוגיות שאף פעם לא ראיתם? זה תלוי ברמת העניין שלכם בטכנולוגיה. כדאי לזכור שבשביל שיהיה למיומנות טכנית ערך כלכלי חייבים לעבור את שלב ה Tutorials ולהגיע לרמה שאתם שולטים טוב בשפה או בטכנולוגיה. לטובים ביותר שפגשתי זה לוקח מינימום שלושה חודשים של עבודה אינטנסיבית בטכנולוגיה זו. סוג כזה של השקעה קשה לבצע כמשהו נוסף על העבודה השוטפת שלכם.

שאלה שחוזרת אצל מתכנתים שמתחילים לעבוד עם JavaScript היא השאלה לגבי sleep. היינו שמחים לפעמים לכתוב טקסט על המסך, לחכות קצת ואז לכתוב עוד קצת טקסט. בתור שפה אסינכרונית ב JavaScript אי אפשר באמת לעצור את ריצת התוכנית בשביל לחכות. אבל גירסת ES8 של השפה הוסיפה תמיכה ב async/await איתם אפשר לבנות את sleep ואפילו די בקלות.

פלאסק היא סביבה לפיתוח Web בשפת Python. זוהי Micro Framework המבוססת על פלאגינים ולכן ליבת הספריה מאוד רזה, מה שאומר שקל מאוד ללמוד איך להתחיל להשתמש בה. באותה נשימה אגיד גם שקל לעשות טעויות. החברה הגדולה ביותר שמצאתי שמשתמשת בפלאסק היא Pinterest שם פלאסק היא הבסיס לשרת ה API של החברה, שרת המטפל במיליארדי בקשות ביום.

בפוסט זה אתאר בקצרה את פלאסק דרך כתיבת מספר תוכניות ונראה מה צריך כדי לבנות Backend API איתו.

כשמורה נותנת לתלמידים שיעורי בית והם חוזרים עם מחברות מלאות לכיתה טבעי הדבר שהמורה תרצה לדעת אם כל אחד פתר לבד. בעולם הקטן של מורים דרך קלה לבדוק את זה היא לבקש מתלמידים שאתה חושד בהם לפתור את התרגיל מול כולם על הלוח. אם התלמיד לא פתר לבד הוא יעמוד לגמגם מול כולם ובגלל הפדיחה יתבייש להעתיק פעם הבאה. או לפחות ככה מספרים.

בהיקף גדול יותר קשה ליישם את הטכניקה הזאת ונדרש פיתרון יותר יצירתי כדי לבדוק שמישהו באמת עשה עבודה בכוחות עצמו. יישום גדול שדורש הוכחת עבודה הוא ביטקוין. אז איך כריית ביטקוין קשורה להוכחת עבודה? ואיזה סוג עבודה צריך לעשות שם?

המפתח בכריית ביטקוין הוא שבניגוד לשיעור הבית שלכם, בביטקוין לכל אחד תרגיל ייחודי משלו. זה מאוד פשוט בעצם: אם לכל תלמיד יש ספר לימוד שמגריל עבורו תרגילים אוטומטית אז אי אפשר להעתיק ואם באת עם הפיתרון כנראה הפיתרון באמת מקורי ושלך. גם בביטקוין לכל אחד יינתן תרגיל ייחודי והראשון שפותר אותו מקבל את המטבע. והתרגיל? הוא דוקא מאוד פשוט להבנה.

מנגנון בשם פונקציית גיבוב (Hash Function) הוא מנגנון שיודע לקחת טקסט כלשהו ולייצר ממנו רצף באורך קבוע של ביטים. הרצף נראה אקראי ואין דרך טובה לדעת מראש איזה טקסט ייתן איזה רצף או לנחש טקסט שייתן רצף ביטים מסוים. לכן תרגיל הוכחת עבודה קלאסי ייראה בערך כך: "מצאו מילה שמתחילה במחרוזת prefix וערך ה Hash שלה מסתיים ב X אפסים".

ניקח כדוגמא את פונקציית ה Hash שנקראת sha1. להלן מספר ערכי Hash למילים נפוצות:

sha1(hello) = aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d
sha1(nice to meet you) = a2a75a27ba7f3f08d2f237b8ca41343a97e27a5e
sha1(abc123) = 6367c48dd193d56ea7b0baad25b19455e529f5ee

קל מאוד לחשב ערך sha1 של מילה מסוימת. אתם יכולים אפילו לבצע את החישוב אונליין למשל באתר הזה:

http://www.sha1-online.com

מצד שני חישוב ערך sha1 של מיליוני מילים כן ייקח זמן ולכן המשימה למצוא מחרוזת שמתחילה במילה מסוימת וערך ה sha1 שלה מסתיים ב X אפסים עשויה להיות מאוד קשה.

ניקח לדוגמא את התחילית ynon. המילים הבאות מתחילות ב ynon ולידן ערכי ה sha1 שלהן שמסתיימים באפסים:

sha1(ynon4)        = a51b25e576433aa9e344fdf660055cbb46be75a0
sha1(ynon280)      = 8efd31f0e08b07edc33feb2f163f600a591b7e00
sha1(ynon9203)     = c2308b862533f09d634f159d3cb2f9788a456000
sha1(ynon27700)    = c1826410aa2800333463b5def882d3218dc00000
sha1(ynon68749224) = 27ebde86cb646b8e24648f97024a37e9fb000000

פה עצרתי. מציאת ערך SHA1 עם שישה אפסים לקח למחשב שלי מעל שתי דקות של עבודה. לא מאמינים לי? יכולים לנסות לחפש בעצמכם ותראו כמה זמן ייקח לכם. מה שיפה כאן שזה בכלל לא משנה איך אתם מחפשים, חשובה רק התוצאה. אחרי שגילינו את הסיומת שתיתן שישה אפסים אפשר לאמת אותה ממש בקלות. הבן אדם הבא שיצטרך לפתור את החידה הזאת לא יצטרך להתאמץ ויוכל פשוט לקחת ממני את הפיתרון.

מצאנו אם כן תרגילים שקל מאוד לייצר אותם (רק צריך להמציא תחילית), קל מאוד להעלות את רמת הקושי שלהם (תגדילו את מספר האפסים שאתם מחפשים) וקל מאוד לבדוק אם תשובה מסוימת היא נכונה. למרות כל זאת קשה מאוד לנחש את התשובה הנכונה ובשביל למצוא אותה מחשבים צריכים לעבוד קשה. תרגילים כאלה מייצרים את הוכחת העבודה הנדרשת לתשתית של מטבעות וירטואליים.

אגב לסקרנים זו התוכנית בשפת רובי איתה מצאתי את ערכי ה SHA1 הרלוונטים:

require 'digest'

class Worker
  attr_reader :prefix, :target_zeros_count, :hasher

  def initialize(prefix, hasher=Digest::SHA256)
    @prefix = prefix
    @hasher = hasher
  end

  def search(target_zeros_count)
    @target_zeros_count = target_zeros_count

    counter = 0
    loop do
      break if ready?(counter)
      counter += 1
    end

    plaintext_with_counter(counter)
  end

  def plaintext_with_counter(counter)
    "#{prefix}#{counter}"
  end

  def hash_with_counter(counter)
    hasher.hexdigest plaintext_with_counter(counter)
  end

  def ready?(counter)
    hash_with_counter(counter).end_with?('0' * target_zeros_count)
  end
end

w = Worker.new(ARGV[0], Digest::SHA1)
res = w.search(ARGV[1].to_i)
puts res
puts Digest::SHA1.hexdigest(res)

לטלגרם יש מנגנון ממש מוצלח של הגדרת שמות משתמש: אתם יכולים לבחור לעצמכם שם משתמש (אני ynonp) ואז אנשים יכולים לדבר אתכם רק דרך שם המשתמש בלי לדעת את הטלפון שלכם.

זה כיף ממש. ואז עולה השאלה למה בכלל צריך מספר טלפון? למה טלגרם צריכים להיות מסוגלים לחבר את החשבון שלי למספר האמיתי שלי? יותר מזה, חשבון ללא מספר טלפון ייתן לנו אפשרות להחזיק מספר חשבונות טלגרם (אחד עסקי ואחד אישי אולי). בקיצור פיצ'ר שהייתי שמח לקבל.

וממילא אם אתם כאן אני מתאר לעצמי שגם אתם הייתם שמחים לקבל. הבעיה שטלגרם לא להוטים לפתוח חשבון לכל אחד בלי מספר טלפון והם בטוחים שמספר טלפון זה איזו הוכחה שאתם אנשים טובים ונחמדים. לכן נעשה תרגיל: נקנה קו טלפון זמני במחיר זול (לא מצאתי בחינם), נחבר אותו לטלגרם ואז נסגור את הקו הזמני. השלבים בפירוט:

  1. מתקינים אפליקציה בשם TextMe Up Second Phone Number ובתוכה קונים מספר טלפון שני. אפשר לבחור מדינה אז כדאי לבחור משהו לא מרכזי מדי כדי שלא תקבלו מספר שכבר אנשים נרשמו איתו. עלות הקו 15 ש"ח.

  2. נכנסים להגדרות טלגרם בטלפון ובוחרים Log Out כדי להתנתק. לאחר מכן נכנסים מחדש עם מספר הטלפון הזמני שקניתם. אליי הסמס לא הגיע אבל אז טלגרם התקשרו והקריאו את הקוד דרך הטלפון (השיחה מנותבת לטלפון שלכם כי קניתם את הקו).

  3. נכנסים עם הקוד שקיבלתם ויש לכם טלגרם.

  4. בשביל שאחרים לא יוכלו להיכנס בשמכם כנסו למסך הגדרות האבטחה ובחרו להגדיר סיסמא (זה נקרא שם כניסה בשני צעדים).

  5. כנסו למסך ההגדרות הכלליות ובחרו לעצמכם שם משתמש לחשבון החדש. מאחר והפעלתם אותו על קו זמני שם המשתמש תהיה הדרך המרכזית שאנשים ימצאו אתכם.

  6. בשביל שיהיה קל לצ'וטט משני החשבונות בלי שתצטרכו כל פעם להתנתק ולהתחבר (זכרו עוד מעט תאבדו את הקו ולא תוכלו להתחבר שוב לעולם לחשבון החדש), כדאי להתקין אפליקציית טלגרם נוספת על המכשיר. יש את Loopy על אייפון ועל אנדרואיד יש אפליקציה בשם Telegram X. התקינו אחת מהן, התחברו ממנה עם החשבון החדש והסיסמא והשאירו אותה מחוברת.

  7. זכרו להתיחס לחשבון החדש כמשהו חד-פעמי. מאחר ומספר הטלפון שלכם יימחק בעוד כמה ימים אחרי שתתנתקו לא תוכלו להתחבר שוב כי לא יהיה לאן לשלוח את הקוד. גבו את השיחות החשובות שניהלתם משם ובחרו שם משתמש שלא תתבאסו לאבד.